Upload engine
uploadToPresigned — the low-level retrying, multipart byte pump.
uploadToPresigned is the raw engine underneath everything else. It takes a file
and a presigned upload descriptor and sends
the bytes to the bucket: a single PUT for small files, parallel multipart for
large ones, with retry, backoff, progress, and abort built in.
Reach for it when you broker the create/complete handshake yourself — for example
a dashboard that already has its own authenticated API client and just needs to
push the bytes. Most apps should use upload (server)
or uploadFiles (browser) instead.
Signature
uploadToPresigned(
file: Blob,
upload: UploadDescriptor,
options?: {
onProgress?: (progress: UploadProgress) => void;
concurrency?: number; // parallel parts (multipart). Default 6.
signal?: AbortSignal;
retries?: number; // default 3
retryDelay?: number; // base backoff ms, default 300
maxRetryDelay?: number; // cap ms, default 10000
},
): Promise<{ parts?: CompletedPart[] }>;The upload descriptor is the upload field returned by createUpload:
type UploadDescriptor =
| { mode: "single"; url: string; method: "PUT" }
| {
mode: "multipart";
uploadId: string;
partSize: number;
parts: { partNumber: number; url: string }[];
};For a single descriptor it returns {}. For a multipart descriptor it returns
{ parts } — the collected part ETags, sorted by part number — which you pass back
to complete.
Brokering the handshake yourself
import { uploadToPresigned } from "s3delivery";
// 1. Reserve via your own authenticated API.
const created = await myApi.createUpload({ filename, contentType, size });
// 2. Push the bytes straight to the bucket.
const { parts } = await uploadToPresigned(file, created.upload, {
onProgress: (p) => setProgress(p.percent),
});
// 3. Confirm.
await myApi.complete(created.file.id, {
uploadId: created.upload.mode === "multipart" ? created.upload.uploadId : undefined,
parts,
});Retry and abort model
This is the engine the retries / retryDelay / maxRetryDelay and signal
options on every higher-level uploader flow into.
- What is retried. Transient failures only: network errors and
5xx/429storage responses. Permanent failures (4xxother than429) and aborts propagate immediately. - Backoff. Exponential with full jitter — each retry waits a random time in
[0, min(maxRetryDelay, retryDelay * 2^n)], up toretriesattempts. - Progress resets between attempts. A failed partial attempt's
loadedcount is reset before the next try, so aggregate progress never double-counts. - Aborts are clean. A fired
signalcancels in-flight parts and pending retries and rejects with anS3DeliveryErrorwhose code isABORTED.
Runtime behaviour
In the browser the engine uses XMLHttpRequest (the only way to observe real
upload progress). In Node, Bun, Deno, and Cloudflare Workers it uses fetch, and
progress fires once on completion.
Multipart needs the ETag exposed
Each multipart part must return its ETag. If your bucket's CORS config does not expose the
ETag response header to the browser, the engine throws NO_ETAG. Make sure the bucket exposes
ETag for cross-origin PUTs — see Large files & multipart.