> ## Documentation Index
> Fetch the complete documentation index at: https://mediadrop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

```ts theme={"theme":{"light":"github-light","dark":"vesper"}}
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 `onProgress` as the upload progresses.** Use `total: null` when
  the length can't be determined.
* **Wire `signal`'s `abort` event to whatever cancellation your transport
  has** (e.g. `XMLHttpRequest.abort()`, `fetch`'s own `signal` support).
* **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 `withRetry` engine (re-exported from `react-mediadrop`)
  for any finer-grained retry the transport itself needs — see
  [Upload](/guides/upload#the-shared-retry-engine). 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?

```ts theme={"theme":{"light":"github-light","dark":"vesper"}}
import { withRetry, 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 new Error(`Upload failed: ${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](/api-reference/xhr-upload).

## What about multi-request transports?

A multi-request transport (splitting one file into several requests,
like a resumable protocol) 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.

Resumable transports (S3 multipart, tus) aren't part of this codebase
today — see [Roadmap](/roadmap).

<CardGroup cols={2}>
  <Card title="Upload" icon="cloud-arrow-up" href="/guides/upload">
    The queue, concurrency, retry, and cancel
  </Card>

  <Card title="Core concepts" icon="cube" href="/guides/core-concepts">
    The file model, the store, and drag state
  </Card>
</CardGroup>
