tus Strategy
WIPResumable uploads over the tus protocol — HEAD for the offset, chunked PATCH, cursor-persisted resume.
TusStrategy implements resumable uploads over the tus protocol. It is
creation-less: your backend creates the tus upload server-side (via the tus creation
extension) and returns a ready upload URL in the intent, so the strategy only performs the
transfer half of the protocol — a HEAD to read the server's Upload-Offset, then a sequence
of PATCH requests. The offset is persisted in the cursor, so a refresh-and-rebind (or a
resumed session on a fresh device) continues where it left off.
import { TusStrategy } from '@gentleduck/upload/strategies'
strategies.set(TusStrategy({ allowedHosts: ['tus.example.com'], chunkSize: 8 * 1024 * 1024 }))
Configuration
TusStrategy({
chunkSize?: number // PATCH chunk size in bytes (default 8 MiB; intent overrides)
allowedHosts?: string[] // lock the tus host (recommended)
allowPrivateHosts?: boolean // allow loopback/RFC1918 hosts (default false)
maxRetries?: number // transient HEAD/PATCH retries (default 3)
})
Intent shape
Your createIntent returns TusStrategy.Intent with a ready upload URL:
type Intent = {
strategy: 'tus'
fileId: string
url: string // ready tus upload URL (backend-created)
chunkSize?: number // optional per-upload chunk override
headers?: Record<string, string> // optional headers on every request (e.g. auth)
}
Because creation happens on the backend, the client never sends Upload-Length /
Upload-Metadata and never parses a Location header — one fewer CORS surface and no presign
logic in the browser.
Cursor shape
The cursor is the last server-acknowledged byte offset:
type Cursor = { offset: number }
type Cursors = { tus?: TusStrategy.Cursor }
How it runs
- Validate the upload URL through the SSRF guard.
- Resolve the offset — start from the persisted cursor, then confirm against the server
with
HEAD(the server'sUpload-Offsetis authoritative on resume). If the transport has nohead(), the persisted cursor is used as-is. - PATCH in chunks — for each slice, send
Upload-Offset,Tus-Resumable: 1.0.0, andContent-Type: application/offset+octet-stream. Trust the server's returnedUpload-Offsetto advance; persist it after each accepted chunk.
// per chunk
const res = await ctx.transport.patch({
url,
body: file.slice(start, end),
headers: {
'Tus-Resumable': '1.0.0',
'Upload-Offset': String(start),
'Content-Type': 'application/offset+octet-stream',
},
signal: ctx.signal,
onProgress: (loaded) => ctx.reportProgress({ uploadedBytes: start + loaded, totalBytes: total }),
})
Each HEAD/PATCH is wrapped in the shared retry helper (backoff on timeouts, 5xx, ECONNRESET).
If the server ever fails to advance Upload-Offset, the strategy bails with an
upload_failed error rather than looping forever.
Transport requirement
The tus strategy uses the transport's patch() for chunk uploads and, when present, head()
to read the resume offset. Both createXHRTransport() (browser) and createFetchTransport()
(Node/SSR/edge) implement them. See Contracts → Transport.
When to use
- Resumable uploads against a tus-compatible server (tusd, tus-node-server, Uppy Companion, …).
- Large files on unreliable connections where offset-based resume beats restarting.
- When you want a single, standardized wire protocol across clients.
For S3-style storage without a tus server, use the Multipart Strategy instead.