Release candidate — 1.0.0-rc.7
StitchAPI
GuidesTransport & adapters

xhrAdapter

Draw an upload progress bar in the browser with an XMLHttpRequest-backed transport, which reports the bytes sent that fetch cannot.

Reach for xhrAdapter() when a browser upload needs a progress bar: fetch cannot report how many bytes have left the machine, and xhr.upload can, so this is the transport that makes a 50MB POST look like something other than a frozen page.

Example

import { ,  } from 'stitchapi';

const  = ({
    : 'POST',
    : 'https://api.example.com',
    : '/videos',
    : { : 'multipart' },
    : (),
});

declare const : File;

await ({
    : {  },
    // `onProgress` is per-call, not config — a different bar per upload.
    : () => {
        if (. === 'upload') {
            .(`${.} / ${. ?? '?'} bytes sent`);
        }
    },
});

Options

xhrAdapter(XHR?) defaults to globalThis.XMLHttpRequest, so in a browser you call it with no arguments. Pass a constructor to run it anywhere else — a polyfill in a non-browser runtime, or a fake that records what a test sent. Off a browser with no constructor supplied, the call rejects with a message naming the fix rather than failing on an undefined global.

import { type , ,  } from 'stitchapi';

// A polyfill or a test double — anything matching the XhrLike shape.
declare const : ;

const  = ({
    : 'POST',
    : 'https://api.example.com',
    : '/users',
    : (),
});

Both progress phases report. xhr.upload.onprogress drives direction: 'upload', xhr.onprogress drives direction: 'download', and each carries loaded plus a total when the length is known. Body encoding and response decoding reuse the same helpers fetchAdapter does, so a multipart body goes out byte-identical on either transport.

Anti-pattern: don't reach for xhrAdapter on a stream or sse surface — it is buffered-only and rejects the call. fetchAdapter is the built-in that streams; the transport is per-stitch, so the uploading stitch can stay on xhr while the streaming one runs on fetch.

An AbortSignal passed on the call aborts the request in flight. A signal that is already aborted rejects before send(), because calling abort() ahead of send() fires no abort event and the cancelled request would otherwise go out anyway.

No redirect guard needed here

The fetch and axios transports rebuild a cross-origin credential strip by hand. This one doesn't need it: the browser follows redirects internally and re-applies CORS to the resulting request, so a custom header like x-api-key or x-amz-* is only sent after the target host opts in through a preflight. JS can't read the Location or intercept the hop, so there is nothing left to strip.

See also

On this page