# ADR 0217: React Native uploads a file as the platform holds it

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0217-react-native-uploads-a-file-as-the-platform-holds-it.md

---

# ADR 0217 — React Native uploads a file as the platform holds it

## Context

ADR 0215 counts upload bytes in one of two places: a stream the transport
pulls, or `XMLHttpRequest` upload events in a browser. Both routes first
encode the body into bytes — `new Request(...).arrayBuffer()`. React Native
cannot take either:

- it has no streaming request body, so the stream route has nothing to send;
- a React Native file part is a `{ uri, name, type }` descriptor inside a
  `FormData`, which the platform streams from disk. Its fetch polyfill cannot
  read such a `FormData` back into bytes, and reading the file to count it is
  what the descriptor exists to avoid;
- the Ky client hands its fetch a `Request`, from which the original
  `FormData` is not recoverable at all.

The chunked driver of ADR 0216 took a DOM `Blob`. A platform file that is not
one — an Expo `File` has `size` and `slice`, and none of the rest of the `Blob`
type — could not be passed without a cast, so a consuming project's mobile
client kept its own copy of the part protocol.

## Decision

1. **A third route, `native-xhr`, chosen when `navigator.product ===
   'ReactNative'`.** The body goes to the platform's `XMLHttpRequest` as the
   client built it: the `FormData` object itself or the JSON string. The Ky
   client passes that body to the wrapper beside the `Request`, since it cannot
   be read back out of it. A `FormData`'s `content-type` is left to the
   platform, which writes its own boundary.
2. **`totalBytes` is known up front for a string or bytes, and comes with the
   first upload event for a `FormData`.** The first report is still `0` and
   the last still equals `totalBytes` — the `0` is emitted with the first
   total, before the first count.
3. **Credentials follow React Native's own fetch:** `include` and `omit` set
   `withCredentials`, anything else keeps the platform default. The browser
   route's refusal of `omit` does not apply — the platform can omit cookies.
4. **An injected fetch in React Native is refused with a `TypeError` at the
   call.** It would take the stream route, which the platform cannot send;
   silently bypassing the injected fetch would skip whatever it adds.
5. **`uploadInChunks` reads a `ChunkSource<TPart>`** — `size` and
   `slice(start, end): TPart` — and a part's `bytes` is what `slice` returned.
   A `Blob` satisfies it unchanged, so existing callers keep their types.

## Consequences

- A mobile client counts a part's upload through the same option as the web
  one, and drives parts through the same driver; its own copy of the protocol
  goes.
- The route is proven by a stand-in XHR in Bun that receives the body object,
  not by a device. A device check is the consuming project's: a part sent with
  `onUploadProgress` reports `0 → … → totalBytes`.
- Detection by `navigator.product` is what React Native sets; a web build of a
  React Native app runs in a browser and takes the browser route.
