# HTTP and large downloads Import `Http` and `Downloads` from `@ps5-react/core`. Binary file bytes remain in native memory; JavaScript receives progress and completion only. ## Enable PS5 networking Add these fields to your app's `app.json`, then build normally: ```json { "networking": true, "filesystemAccess": "console" } ``` The initial native-title transport requires successful console filesystem elevation (resident Lapy or a localhost ELF loader, as described in [NATIVE-API.md](NATIVE-API.md#hardware-status)). It uses the console trust store at `/system/common/cert/CA_LIST.cer`, verified HTTPS, SceNetResolver DNS and BSD sockets with native-title nonblocking compatibility. Failed elevation or missing trust certificates leaves networking unavailable with an actionable error. Desktop networking uses macOS libcurl and its trust configuration; it is available without the PS5 opt-in. Desktop headers and libraries require libcurl 7.85 or newer; install a current curl development package if CMake reports an older version. The optional build downloads the SHA-256-pinned PacBrew v0.40.2 networking archive. It extracts only networking headers/libraries, preserving the main v0.42 SDK. See [DEPENDENCIES.md](DEPENDENCIES.md). **Hardware status:** compilation and desktop tests do not establish PS5 network operation, throughput, heap headroom or firmware compatibility. The networking artifact still requires its own [hardware evidence](HARDWARE.md). ## Download a large file ```js import {Downloads, FileSystem} from '@ps5-react/core'; const task = Downloads.enqueue({ url: 'https://example.com/archive.bin', destination: `${FileSystem.dataDir}/archive.bin`, // sha256: '64 hexadecimal digits from a trusted manifest', }); const unsubscribe = task.subscribe(progress => { console.log(progress.state, progress.written, progress.total); }); try { await task.done; } finally { unsubscribe(); } ``` The destination's parent directory must exist. The task writes `destination.part` and maintains `destination.resume` for eligible ranges. The destination must not already exist unless `recoverCompleted` can verify its completion receipt. Keep the destination and sidecars exclusively owned by the task; filesystem APIs and other processes must not rename, edit or create them while it runs. Successful completion syncs and renames the partial file. Failed or cancelled tasks preserve partial data. Metadata syncs precede checkpoint rename; parent directories are synced where supported. Filesystems that reject directory fsync cannot provide the same power-loss durability. Missing or incompatible checkpoints fail safely. Publication uses an exclusive hard link where available; exFAT reserves the filename with `O_EXCL` before rename. Keep exclusive ownership of that reservation; other processes must not replace it. `task.cancel()` requests cancellation. `task.done` rejects with an `AbortError` on cancellation and an `Error` on failure. Each task exposes `id`, `snapshot`, and `subscribe(listener)`, which immediately supplies the current snapshot and returns an unsubscribe function. In a React effect, unsubscribe and cancel the task during cleanup if its lifetime belongs to that component. | Option | Default | Limits and behavior | | --- | --- | --- | | `connections` | 8 | Integer 1–64; maximum ranges in flight | | `mirrors` | omitted | Up to 4 other http(s) URLs for the same file. Each is probed and kept only if its size and strong ETag match the primary (or a `sha256` is given); ranges then rotate over the primary and the mirrors, and a mirror that fails a range is dropped. Not combined with `pieces` | | `adaptive` | `true` | Per-origin congestion window (AIMD) under `connections`: starts at up to 8 and grows by half each second while full and healthy; HTTP 5xx/429/408 or a broken connection halves it (Retry-After also pauses that origin), after which it grows by one a second. `false` keeps `connections` fixed | | `rangeBytes` | 32 MiB | Integer 1–256 MiB; at most 65,536 ranges | | `resume` | `true` | Reuses matching durable completed ranges; `false` refuses existing partial files | | `rejectHtml` | `false` | Rejects HTML content types and initial HTML signatures before a file can be published; provider pages are not binary downloads | | `recoverCompleted` | `false` | Writes a durable `.complete` receipt before publication; a later identical request accepts its owned final file (re-hashing it only against a requested `sha256`) instead of downloading it again | | `sha256` | omitted | Trusted 64-digit SHA-256; verifies the complete file in the writer thread before publication | | `expectedBytes` | omitted | Exact positive safe-integer file size; rejects mismatched server sizes before downloading | | `storageRoot` | omitted | Allowed absolute directory containing the destination; guards device/inode identity during writes and before publication | | `headers` | omitted | Up to 32 string header values; see HTTP restrictions below | A one-byte GET probe checks range support and size. A server that answers it with a 206 range downloads in parallel and can resume. A strong ETag guards every range through `If-Range`; without one, `Last-Modified` does (weak ETags are ignored); without either, only the exact size and range bounds are checked. Ranges reuse the probe's final URL after redirects, resolving the original URL again on a 4xx or on a range's second 5xx there; the first range that lands through the new redirect pins its target. Every range checks its exact bounds, total, status and validator. Only an ETag, Last-Modified or total that differs on the URL the validators came from fails the task as "server changed the resource"; a 200 that ignores `Range` or a 206 with the wrong span is retried up to three attempts, and so is a validator difference on a range that a fresh redirect sent to another node. A server without range support uses a single sequential connection; its partial files cannot resume. Resume requires the same validator, hash, size, range size and storage root, not the same URL: a signed link that expired can be resolved again and the partial continues. Without a strong ETag, a resume first downloads the last 64 KiB of the last completed range again and compares it with the partial file; a difference fails with "the server's file changed since this partial download". Only completed, synced ranges survive resume; unfinished ranges download again. With `rejectHtml`, HTTP 401/403 fails with "provider denied the file; link expired or browser verification required", so an app can resolve its source again. Hash verification performs one final sequential file read because ranges arrive out of order. Matching validators are consistency checks, not cryptographic verification of local partial data. With `recoverCompleted: true`, the writer persists `destination.complete` after verification and file sync, before publishing the final filename. The receipt records the request identity, the selected root, the final inode and size, and the SHA-256 when one was checked. Recovery matches the request (hashes and expected size; not URLs), selected-root identity, final inode and size. It re-reads the entire file only when the request carries a `sha256` (or piece SHA-1s) to check; otherwise the receipt is trusted, so recovering a finished file of tens of GB costs no read-back. Without a hash, verification proves the file is the one this task published, not its content. Recovery does not contact the provider or accept an existing file by name/size alone. Keep the receipt with the final file for crash recovery. A sequential partial without a checkpoint cannot resume. An app may offer an explicit restart that discards its exclusively owned partial and sidecars after cancellation has finished. Never discard or overwrite an existing final file to make a retry succeed. ## Split-file manifests and format routing ```js import {DownloadFormats, Downloads, FileSystem} from '@ps5-react/core'; const artifact = DownloadFormats.artifact({format: 'ffpfsc', filename: 'game.ffpfsc'}, '/data'); FileSystem.mkdir(artifact.directory, {recursive: true}); const task = Downloads.enqueueManifest({manifest, destination: artifact.destination, storageRoot: '/data'}); await task.done; ``` `DownloadFormats.manifest` validates Spectrum's `originalFileSize`, `numberOfSplitFiles`, `packageDigest` (SHA-256), and `pieces` with `url`, `fileOffset`, `fileSize`, and optional `hashValue` (SHA-1). Pieces are sorted and must cover the exact size without gaps or overlaps. The limit is 1024 sources and 65,536 transfer ranges. `enqueueManifest` normalizes this schema and submits the same native service as `enqueue`. Alternatively, `enqueue` accepts `pieces: [{url, offset, size, sha1}]` together with `expectedBytes`. Each source is probed separately. Sources supporting ranges use source-relative HTTP ranges; writes use absolute output offsets. Sources without range support use one whole-piece request. Multiple pieces can be in flight within the same 1–64 connection and 16 MiB buffer limits. No complete temporary copy of each piece is required. The final writer verifies SHA-1 pieces and optional whole-file SHA-256 in one sequential read before publication. SHA-1 here checks legacy manifest integrity; it is not a modern authenticity proof. Each source needs a strong ETag, piece SHA-1, or whole-file SHA-256. The checkpoint identity includes every offset, size, hash and validator, but no URL. Changed manifests or storage identities cannot reuse a checkpoint. Completed ranges of whole-piece fallbacks resume only once that entire piece has been durably written. Inputs must describe raw byte slices; independent RAR volumes are not such a manifest. `DownloadFormats.artifact({format, filename}, root)` validates the filename and returns `directory`, `destination`, `format` and `nextAction`: | Formats | Relative destination | Next action | | --- | --- | --- | | `ffpkg`, `exfat`, `ffpfs`, `ffpfsc` | `homebrew/` | `shadowmount` | | `pkg`, `fpkg` | `downloads/packages/` | `installer-required` | | `zip`, `rar`, `7z`, `tar`, `tar.gz`, `tgz` | `downloads/archives/` | `extraction-required` | | Other binary files | `downloads/files/` | `downloaded` | `DownloadFormats.detected({filename}, root, kind)` routes a file whose catalog format was unknown by the `kind` `Archives.inspect` read from its bytes; the filename takes that extension in place of `.bin`/`.binary`. Routing checks declared format against known filename extensions; it does not parse filesystem/package headers or certify mountability. `.fpkg` is a package, whereas `.ffpkg` is a UFS image. A raw `.part0`/`.001` file cannot be handed to ShadowMount directly. Extracted application folders remain the existing build workflow; they are not a single HTTP artifact. No archive extractor or PKG installer is included. Successful downloading is not installation or registration. With `storageRoot`, the worker holds a directory descriptor, checks its device and inode against the path at most once per second and immediately before publication, and requires the partial to remain on that filesystem. Resume identities include the original root identity. A disconnect can allow up to one second of writes to the open file before detection; it never redirects those writes to another storage root. Apps still need to check free space and actual mounted external roots before submission. Write failures preserve partial data. Snapshots contain `state`, `received`, `written`, `total` (bytes or `null`), `bytesPerSecond`, `connections`, `retries`, `bufferedBytes`, `status`, `id`, `destination` and `error`. States are `queued`, `connecting`, `downloading`, `verifying`, `completed`, `failed`, and `cancelled`. Progress is sampled and polled every 250 ms; retrying a range can reduce progress. `written` is completed ranges plus bytes written in current ranges, not bytes already checkpointed. `connections` includes ranges draining to disk, rather than an exact socket count. ## Bounded HTTP requests ```js import {Http} from '@ps5-react/core'; const response = await Http.request('https://example.com/catalog.json', { method: 'GET', maxBytes: 1024 * 1024, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const catalog = await response.json(); ``` `method` supports uppercase GET, HEAD, POST, PUT, PATCH and DELETE. `body` is a UTF-8 string up to 1 MiB. `maxBytes` defaults to 1 MiB and accepts 1 byte–4 MiB. Requests have a 30-second total timeout. The result exposes `status`, `ok`, the effective `url`, a frozen `headers` object with lowercase names, async `text()` and async `json()`. Non-2xx responses resolve normally; transport and response-size failures reject. Only content-type, content-length, content-disposition, content-range, location, hx-redirect, etag and retry-after are exposed. Redirect response headers are cleared before collecting the final response. Cookies and authentication headers are not exposed; binary request or response bodies are not exposed. A compatible `signal` with `aborted`, `addEventListener` and `removeEventListener` can cancel requests; the framework does not install an AbortController or global `fetch`. URLs must use HTTP or HTTPS and be at most 8192 bytes. Headers have token names up to 128 bytes and string values up to 2048 bytes, without CR/LF. Range, If-Range, Accept-Encoding, Content-Length, Connection and Host are owned by the transport and cannot be overridden. Custom headers disable automatic redirects; use the final URL for authenticated requests. `followRedirects: false` also disables redirect following, making the allowlisted Location/HX-Redirect metadata available for provider adapters. Otherwise redirects are limited to five, and HTTPS cannot downgrade to HTTP. TLS peer and hostname verification are always enabled. Responses use identity encoding. Remote images (``, [IMAGES.md](IMAGES.md)) use the same transport policy on their own workers and connections, outside this queue. ## Performance and resource contract One persistent libcurl multi handle and reusable easy handles share connections. HTTP/1.1 limits buffering associated with paused multiplexed streams. A 16 MiB pool of 32 × 256 KiB blocks bounds application file buffers, independently of file size and concurrency. A network worker and a disk writer each use a 1 MiB stack; TLS, curl internals, response strings and metadata consume additional memory in the title's one process heap. These are not total-memory bounds. The writer uses 64-bit-offset `pwrite`, and buffer exhaustion pauses network transfers until the writer returns blocks. Checkpoints run every five seconds and at completion/cancellation. Connection failures (reset, timeout, refused, unresolved, truncated body) and HTTP 408/429/5xx are a busy server, not a damaged file: a range retries up to eight total attempts (a probe six), with exponential delay from 0.5 s to 30 s, jitter, and numeric Retry-After honored up to a minute. HTTP-date Retry-After is not supported. Each failed range logs its status, Content-Range, Content-Length, ETag, encoding and final URL. The default concurrency is a starting point, not a measured PS5 optimum. The FIFO queue accepts eight unconsumed tasks, with **one task active at a time**; a large file uses multiple connections within that task. HTTP requests queued behind that file wait for it to finish. Polling consumes terminal snapshots and frees queue slots. Hosts cancel and join both workers before destroying QuickJS. Workers never access JavaScript, the engine or controller state; native bridge calls synchronously submit/cancel/poll bounded work on the render thread. Run `npm test` for localhost native transfer tests, JS task lifecycle tests and existing UI/controller checks. No tests contact a console or download a catalog. Use [DOWNLOAD-PERFORMANCE.md](DOWNLOAD-PERFORMANCE.md) for research and the hardware benchmark plan. No PS5 download-speed measurement is claimed.