Writing a custom transport
Implement the UploadTransport contract for anything the reference XHR transport doesn't cover
react-mediadrop/xhr-upload covers a generic REST-ish endpoint. For
anything else — a provider SDK, a test double, a more advanced protocol —
write your own transport against the same contract:
type UploadTransport = {
upload(
file: MediaDropFile,
context: {
onProgress: (progress: { loaded: number; total: number | null }) => void;
signal: AbortSignal;
},
): Promise<{ response?: unknown }>;
};
What must a transport do?
- One file, one attempt. Send the file, report progress, resolve or reject. The queue decides whether to retry — you don’t.
- Call
onProgressas the upload progresses. Usetotal: nullwhen the length can’t be determined. - Wire
signal’sabortevent to whatever cancellation your transport has (e.g.XMLHttpRequest.abort(),fetch’s ownsignalsupport). - Resolve with
{ response }— anything, opaque to the engine (e.g. the server’s parsed JSON body) — on success. Reject on failure. - Do not implement your own retry or backoff inside the transport.
Use the shared
withRetryengine (re-exported fromreact-mediadrop) for any finer-grained retry the transport itself needs — see Upload. This is a direct reaction to a real anti-pattern: libraries that let every transport carry its own independent copy of retry/backoff logic end up with subtly different retry behavior per transport, and no single place to fix a bug in it. react-mediadrop has one retry engine, and this is where you plug into it. - Do not implement your own concurrency limit. The queue decides how many uploads run at once; your transport just services one call at a time when asked.
What does a minimal transport look like?
import { createHttpError, type UploadTransport } from "react-mediadrop";
function createMyTransport(options: { endpoint: string }): UploadTransport {
return {
async upload(file, { onProgress, signal }) {
const response = await fetch(options.endpoint, {
method: "POST",
body: file.file,
signal,
});
if (!response.ok) {
throw createHttpError(`Upload failed: ${response.status}`, response.status);
}
onProgress({ loaded: file.size, total: file.size });
return { response: await response.json() };
},
};
}
This example reports progress only once, at completion, because fetch
has no cross-browser upload-progress API — this is exactly why
react-mediadrop/xhr-upload uses XMLHttpRequest instead. If your task
needs real incremental progress, use XMLHttpRequest.upload.onprogress
the way the reference transport does — see its
API reference.
What about multi-request transports?
A transport that splits one file into several requests is still “one
file, one upload() call” from the queue’s point of view — internally
it may issue many requests and retry individual ones via the shared
withRetry, called again for that finer-grained retry, but never a
second, hand-rolled retry implementation.