# Constraints

Constraints are the two limits an upload route enforces before it signs anything: how big a file may be, and what type it may be.

```ts lib/uploads.ts
import { uploadHandler, uniquePath } from "@upstash/blob"

export const uploads = uploadHandler({
  constraints: { maxSize: "20mb", contentTypes: ["image/*", "application/pdf"] },
  onBeforeUpload: ({ file }) => ({ path: uniquePath`uploads/${file.name}` }),
})
```

They are checked at `begin`, from the name, type and size the browser declared, before `onBeforeUpload` runs. A refused file leaves nothing behind: no presigned URL, no row, no multipart upload.

Omitting `constraints` accepts any type at any size.

---

## maxSize

```ts
constraints: { maxSize: "2mb" }   // 2,000,000 bytes
constraints: { maxSize: 4096 }    // a bare number is bytes
```

`maxSize` takes a [Size](/blob/reference/types#size). Sizes are decimal, so `'2mb'` is 2,000,000 and `'5mib'` throws. A refusal reads back in the same units:

```
cat.png is 2.4 MB, over the 2 MB limit
```

An unparseable size throws a `TypeError` where the option is written, at startup, not once per request.

---

## contentTypes

```ts
constraints: { contentTypes: ["image/*", "application/pdf", "image/svg+xml"] }
```

Each entry is either an exact `type/subtype`, or one of three wildcards: `image/*`, `video/*` and `audio/*`.

Anything else throws `invalid_content_type_pattern`: `*/*`, `text/*`, and strings that are not a media type (`png`, `image/`, `/png`). An empty list throws too; omit the option to accept anything.

### What the wildcards expand to

| Wildcard  | Expands to |
| --------- | ---------- |
| `image/*` | `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/bmp`, `image/tiff`, `image/avif`, `image/heic`, `image/heif`, `image/x-icon` |
| `video/*` | `video/mp4`, `video/quicktime`, `video/webm`, `video/x-matroska`, `video/x-msvideo`, `video/mpeg`, `video/ogg`, `video/3gpp` |
| `audio/*` | `audio/mpeg`, `audio/wav`, `audio/ogg`, `audio/opus`, `audio/flac`, `audio/aac`, `audio/mp4`, `audio/webm` |

<Note>
  `image/*` deliberately does not include `image/svg+xml`. An SVG can contain script, so allow it explicitly by listing `'image/svg+xml'`.
</Note>

### Aliases

Browsers and operating systems send several spellings for some types. Both the declared type and your list are canonicalized before comparison, so `contentTypes: ['image/jpg']` accepts a file declared `image/jpeg`, and the reverse. Parameters are stripped, so `image/png; charset=binary` is `image/png`.

<Accordion title="Alias table">
| Written | Canonicalizes to |
| ------- | ---------------- |
| `image/jpg` | `image/jpeg` |
| `image/pjpeg` | `image/jpeg` |
| `image/vnd.microsoft.icon` | `image/x-icon` |
| `audio/x-wav` | `audio/wav` |
| `audio/wave` | `audio/wav` |
| `audio/vnd.wave` | `audio/wav` |
| `audio/mp3` | `audio/mpeg` |
| `audio/x-flac` | `audio/flac` |
| `audio/x-aac` | `audio/aac` |
| `video/avi` | `video/x-msvideo` |
| `video/msvideo` | `video/x-msvideo` |
| `application/x-gzip` | `application/gzip` |
| `application/x-zip-compressed` | `application/zip` |
| `application/vnd.rar` | `application/x-rar-compressed` |
</Accordion>

---

## Byte sniffing

A route with `contentTypes` checks more than the declared type. The browser sends the file's leading bytes with the `begin` request, so a mislabelled file is refused before the upload rather than after it. Two checks run:

1. **The declared type must be in the allow list.** A `report.exe` declared as `application/x-msdownload` is refused here.
2. **The leading bytes must not clearly contradict the declared type.** If the bytes prove nothing, the file passes.

The second check is lenient on purpose so real files are not refused. A `.docx` really is a zip, and so are `.epub`, `.jar` and `.apk`. Formats that share a signature, like mp4 and heic, or webm and mkv, never contradict a declaration. Neither does `application/octet-stream`.

<Note>
  This is a convenience, not a security control. The bytes never reach your server, so a client can send an honest sample and then upload something else. It is not malware scanning, and stored objects should still be treated as untrusted.
</Note>

---

## Per-route constraints

```ts lib/uploads.ts
export const uploads = uploadHandler({
  constraints: { maxSize: "20mb", contentTypes: ["image/png"] },
  routes: {
    attachment: {
      onBeforeUpload: () => ({ path: "attachment/1.png" }),
    },
    avatar: {
      constraints: { maxSize: "2mb" },
      onBeforeUpload: () => ({ path: "avatar/demo" }),
    },
    large: {
      constraints: { maxSize: "2gb", contentTypes: null },
      onBeforeUpload: () => ({ path: "large/1.bin" }),
    },
  },
})
```

| Route | `maxSize` | `contentTypes` |
| ----- | ---------- | -------------- |
| `attachment` | 20,000,000, inherited | `['image/png']`, inherited |
| `avatar` | 2,000,000, replaced | `['image/png']`, inherited |
| `large` | 2,000,000,000, replaced | none, cleared by `null` |

A route's `constraints` **override** the handler's key by key. A key the route does not mention is inherited. `null` clears a key the handler set.

---

## Narrowing per user

```ts lib/uploads.ts
onBeforeUpload: async ({ request, file }) => {
  const user = await getUser(request)
  return {
    path: uniquePath`${user.id}/${file.name}`,
    constraints: user.plan === "free" ? { maxSize: "25mb", contentTypes: ["image/*"] } : undefined,
  }
},
```

`onBeforeUpload` may return `constraints` to tighten the route's limits for this one upload, once it knows who is uploading. They are checked against the same file right after it returns. Widening throws a `TypeError`, so a route's code always shows the most it can accept.

---

## In the browser

```json
{ "constraints": { "contentTypes": ["image/png"], "maxSize": 2000000 } }
```

`GET` on the upload route serves the constraints it enforces, cached for 60 seconds, so a deploy that changes a route's limits reaches clients within a minute.

`useUpload` exposes two things from it. `accept` is `contentTypes` joined with commas, ready for an `<input accept>`. `constraints` is the served document itself, so a page can show the limit it enforces.

```tsx components/upload-button.tsx
"use client"
import { formatBytes } from "@upstash/blob/react"
import { useUpload } from "@/lib/upload-hooks"

export function UploadButton() {
  const { start, upload, accept, constraints } = useUpload()

  return (
    <>
      <input type="file" accept={accept} onChange={(e) => start({ file: e.target.files?.[0] })} />
      {constraints?.maxSize !== undefined && <p>Up to {formatBytes(constraints.maxSize)}</p>}
      {upload?.error && <p>{upload.error.message}</p>}
    </>
  )
}
```

A file over `maxSize` is refused in the browser before any request is made. It still becomes a record with `status: 'error'` and a `BlobError` with code `too_large`, so one error path renders both the client-side refusal and the server's.

The size check is the only one that runs in the browser. **The server is authoritative** for everything else. If the constraints have not arrived yet, the file is simply sent and the route decides.

---

## Error codes

A refusal here is `too_large`, `content_type_not_allowed`, `invalid_content_type_pattern` or `empty_body`. It reaches the browser as a `BlobError` with that code intact, so switch on `error.code` rather than on status numbers. See [Errors](/blob/reference/errors#the-codes).

---

## Server-side writes

`bucket.put()` takes the same `contentTypes` and `maxSize` options, with the same grammar, aliases and byte check, for bytes that pass through your own route. See [Writing](/blob/bucket/writing#put).
