Uploads & Downloads
Upload FormData, File, Blob and binary bodies with @mrzr/api-client, keep long uploads from failing on token expiry, and download files as a Blob.
Uploading
Pass the body as-is. The client doesn't JSON-encode it and picks the right Content-Type:
const form = new FormData();
form.append("avatar", file);
await api.post("/me/avatar", form); // multipart, boundary set by the browser
await api.put("/files/1", file); // File or Blob: sent as raw bytes
await api.post("/raw", new Uint8Array([1, 2, 3]));
await api.post("/login-form", new URLSearchParams({ user, pass }));| Body | Sent as |
|---|---|
| Plain object or array | JSON |
FormData | multipart/form-data |
File, Blob | Raw bytes with the file's type |
ArrayBuffer, typed arrays, DataView | Raw bytes |
URLSearchParams | application/x-www-form-urlencoded |
ReadableStream | Streamed (see below) |
string | As-is, with Content-Type: application/json unless you set another |
Which Content-Type wins
- A header you set on the request.
- A client-wide header that isn't
application/json. It's treated as deliberate. - Otherwise the body's own type: the client-wide JSON default is dropped for files, bytes and forms.
FormData always gets the browser's multipart boundary. A hand-written multipart/form-data header without boundary= is removed, because it would corrupt the upload.
await api.post("/upload", pngBytes, { headers: { "Content-Type": "image/png" } });Long uploads
The default timeout is 30 s. Raise it, or turn it off, for big files:
await api.post("/upload", form, { timeout: 10 * 60_000 });
await api.post("/upload", form, { timeout: 0 }); // no limitIf the token expires mid-upload, the server answers 401 and the client refreshes and re-sends the file automatically. To avoid uploading twice, refresh before starting when the token won't last the whole upload:
await api.post("/upload", form, { uploadSkewMs: 10 * 60_000 });For very large files, upload in chunks. Each chunk is a normal request with its own timeout and refresh handling, and you get progress and resuming for free:
const CHUNK = 5 * 1024 * 1024;
for (let i = 0; i * CHUNK < file.size; i++) {
await api.post("/upload/chunk", file.slice(i * CHUNK, (i + 1) * CHUNK), {
params: { uploadId, index: i },
headers: { "Content-Type": "application/octet-stream" },
});
setProgress(Math.min(1, ((i + 1) * CHUNK) / file.size));
}
await api.post("/upload/complete", { uploadId });Streams
A ReadableStream body works only with worker: false, because a stream can't be passed into a Web Worker. In worker mode you get a clear error instead: A ReadableStream body cannot be sent through a Web Worker. Send a Blob or File, or create a separate client for streaming:
const streamApi = createClient({ baseUrl, worker: false });
await streamApi.post("/upload", stream, { uploadSkewMs: 10 * 60_000, timeout: 0 });A stream also can't be replayed after a 401. The request fails with a 401 that says so, but the token has been refreshed, so sending a fresh stream succeeds. uploadSkewMs avoids the problem in the first place.
Downloading files
Non-text responses (images, PDFs, zip files, Office files such as xlsx and docx) resolve as a Blob:
const { data } = await api.get<Blob>("/reports/42.pdf");
const url = URL.createObjectURL(data!);Pass responseType: "blob" or "arrayBuffer" to force a format. A "save as" download goes through the client, so it's authenticated and refreshes like any request:
export async function download(path: string, filename: string) {
const { data } = await api.get<Blob>(path, { responseType: "blob", timeout: 0 });
const url = URL.createObjectURL(data!);
Object.assign(document.createElement("a"), { href: url, download: filename }).click();
URL.revokeObjectURL(url);
}When a download fails with a JSON or text body, that body is still parsed, so message, errors and ApiError.data hold the server's error:
try {
const { data } = await api.post<Blob>("/reports/export", filters, { responseType: "blob" });
save(data!);
} catch (error) {
if (error instanceof ApiError) toast(error.message); // the server's message, not a Blob
}Progress
fetch has no upload progress event, so the client can't report one. Chunked uploads (above) give you progress per chunk. For a byte-level progress bar, use XMLHttpRequest for that one upload and the client for everything else.
Cancellation
Cancel in-flight requests when the user leaves a page, closes a modal or types the next search keystroke. Cancel by key, group, URL pattern or scope.
Web Worker & Tabs
How @mrzr/api-client keeps access tokens inside a Web Worker away from XSS, when it falls back to the main thread, and how tabs share login and logout.