# Browser uploads (/docs/browser-uploads)



`uploadFiles` is the framework-free browser uploader. It talks only to your
mounted [route handler](/docs/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`](/docs/react#useuploadfile) /
[`useUploadFiles`](/docs/react#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.

```ts
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 [#signature]

```ts
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`](/docs/errors).

## Options [#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]

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.

```ts
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` |

<Callout type="warn" title="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`](/docs/route-handler#constraints) for
  the authoritative check.
</Callout>

`allowedFileTypes` matching is case-insensitive, ignores a `; charset=…` suffix,
and supports `"*"` / `"*/*"` (allow all), `"image/*"` (any subtype), and exact
types like `"application/pdf"`.

## Concurrency [#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.

```ts
await uploadFiles(files, { concurrency: 5, partConcurrency: 8 });
```

## Retries & backoff [#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]

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).

```ts
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 [#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`.

```ts
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 [#auth-headers]

If your route handler authenticates requests, send credentials with `headers`:

```ts
await uploadFiles(files, {
  headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
});
```

The function form runs before each handshake request, so you can refresh a token.
