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 aFormData, which the platform streams from disk. Its fetch polyfill cannot read such aFormDataback 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 originalFormDatais 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
- A third route,
native-xhr, chosen whennavigator.product === 'ReactNative'. The body goes to the platform’sXMLHttpRequestas the client built it: theFormDataobject itself or the JSON string. The Ky client passes that body to the wrapper beside theRequest, since it cannot be read back out of it. AFormData’scontent-typeis left to the platform, which writes its own boundary. totalBytesis known up front for a string or bytes, and comes with the first upload event for aFormData. The first report is still0and the last still equalstotalBytes— the0is emitted with the first total, before the first count.- Credentials follow React Native’s own fetch:
includeandomitsetwithCredentials, anything else keeps the platform default. The browser route’s refusal ofomitdoes not apply — the platform can omit cookies. - An injected fetch in React Native is refused with a
TypeErrorat the call. It would take the stream route, which the platform cannot send; silently bypassing the injected fetch would skip whatever it adds. uploadInChunksreads aChunkSource<TPart>—sizeandslice(start, end): TPart— and a part’sbytesis whatslicereturned. ABlobsatisfies 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
onUploadProgressreports0 → … → totalBytes. - Detection by
navigator.productis what React Native sets; a web build of a React Native app runs in a browser and takes the browser route.