Browser uploads
uploadFiles — parallel files, multipart, retries, and progress.
uploadFiles is the framework-free browser uploader. It talks only to your
mounted route handler for the create/complete handshake
(so your API key stays server-side), then sends the bytes straight to the
bucket. Files and multipart parts upload in parallel, every PUT retries on
transient failures, and progress is reported per-file and in aggregate.
The React hooks (useUploadFile /
useUploadFiles) wrap this function — reach for
them in React. Use uploadFiles directly when you are not on React or want to
manage your own state.
import { uploadFiles } from "s3delivery";
const files = await uploadFiles(input.files, {
maxFileSize: 10 * 1024 * 1024,
allowedFileTypes: ["image/*"],
onProgress: (p) => setProgress(p.percent),
});
console.log(files[0].url);Signature
uploadFiles(
input: File[] | FileList,
options?: UploadFilesOptions,
): Promise<S3DeliveryFile[]>;It resolves with the stored files in the same order as the input selection.
An empty selection resolves to []. On any failure it rejects with an
S3DeliveryError.
Options
| Option | Default | Notes |
|---|---|---|
endpoint | "/api/s3delivery" | URL of your mounted route handler. |
headers | — | Extra headers for the route-handler requests (e.g. auth). Object or a (possibly async) function returning one. |
visibility | route default | "private", "public", or "unlisted". |
maxFiles | — | Reject if more than this many files are selected (TOO_MANY_FILES). |
maxFileSize | — | Reject any file larger than this, in bytes (FILE_TOO_LARGE). |
allowedFileTypes | — | Allowed MIME types; exact or type/* wildcards (TYPE_NOT_ALLOWED). |
concurrency | 3 | Files uploaded in parallel. |
partConcurrency | 6 | Multipart parts uploaded in parallel, per file. |
retries | 3 | Max retry attempts per PUT on transient failures. |
retryDelay | 300 | Base backoff in ms (doubles each attempt, with full jitter). |
maxRetryDelay | 10000 | Cap on any single backoff wait, in ms. |
onProgress | — | Aggregate progress across all files. |
onFileProgress | — | (file, progress) => void per file. |
onFileComplete | — | (result, file) => void as each file finishes. |
signal | — | AbortSignal to cancel the whole batch. |
Constraints
Constraints are checked client-side, before any URL is requested, so a too-large or wrong-type file fails instantly and locally — no wasted round-trip.
await uploadFiles(input.files, {
maxFiles: 10,
maxFileSize: 25 * 1024 * 1024,
allowedFileTypes: ["image/*", "application/pdf"],
});Each violation throws an S3DeliveryError with a precise code:
| Constraint | Error code |
|---|---|
maxFiles | TOO_MANY_FILES |
maxFileSize | FILE_TOO_LARGE |
allowedFileTypes | TYPE_NOT_ALLOWED |
Client constraints are convenience, not security
These give fast, friendly feedback, but a determined client can bypass them. Mirror maxFileSize
/ allowedFileTypes in your route handler constraints for
the authoritative check.
allowedFileTypes matching is case-insensitive, ignores a ; charset=… suffix,
and supports "*" / "*/*" (allow all), "image/*" (any subtype), and exact
types like "application/pdf".
Concurrency
Two independent dials, both tuned to be fast by default:
concurrency(default 3) — how many files upload at once. A batch is not bottlenecked on one slow file.partConcurrency(default 6) — for a large file, how many 16 MB parts upload at once. Multipart kicks in automatically above the server threshold; you do not configure when.
await uploadFiles(files, { concurrency: 5, partConcurrency: 8 });Retries & backoff
Every PUT retries on transient failures — network errors and 5xx / 429
responses — using exponential backoff with full jitter. Permanent failures
(4xx other than 429) and aborts are never retried.
| Option | Default | Meaning |
|---|---|---|
retries | 3 | Max retries per PUT (so up to 4 total attempts). |
retryDelay | 300 | Base delay in ms; the backoff ceiling doubles each attempt. |
maxRetryDelay | 10000 | Cap on any single wait. |
Each attempt waits a random time in [0, min(maxRetryDelay, retryDelay * 2^n)].
Progress for a part resets between attempts, so the aggregate never double-counts
a partially-uploaded failed try.
Progress
Progress arrives as { loaded, total, percent } (percent is 0–100, rounded). In
the browser, uploads use XMLHttpRequest to report real upload progress; in
Node, Bun, Deno, and Workers it falls back to fetch (progress fires once at
completion).
await uploadFiles(files, {
onProgress: (p) => setAggregate(p.percent),
onFileProgress: (file, p) => setPerFile(file.name, p.percent),
onFileComplete: (result, file) => console.log(`${file.name} → ${result.url}`),
});Aborting
Pass an AbortSignal to cancel a batch. In-flight parts and pending retries
cancel immediately, and the promise rejects with an S3DeliveryError whose code
is ABORTED.
const controller = new AbortController();
const promise = uploadFiles(files, { signal: controller.signal });
cancelButton.onclick = () => controller.abort();
try {
await promise;
} catch (e) {
if (e instanceof S3DeliveryError && e.code === "ABORTED") {
// user cancelled
}
}In React, prefer the hooks' control.abort() — same effect, no manual controller.
Auth headers
If your route handler authenticates requests, send credentials with headers:
await uploadFiles(files, {
headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
});The function form runs before each handshake request, so you can refresh a token.