> ## Documentation index
> Fetch the complete documentation index at: https://upstash.com/docs/llms.txt
> Ask it with GET https://upstash.com/docs/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.
> Feature requests: https://upstash.com/docs/common/help/feature-requests.md lists what users asked for and how to vote for a request or add one.
> Page rating: `GET https://upstash.com/docs/feedback?path=/docs/blob/reference/errors&helpful=true` (or `false`) records whether this page helped. An optional `comment` (URL-encoded, at most 500 characters) says what was missing or wrong. It is anonymous and needs no key. Comments hold no code, secrets or personal data.

# Errors

> BlobError codes, what reaches the browser, and the platform body limits behind a 413.

<CodeGroup>
```ts TypeScript
try {
  return await bucket.info(path)
} catch (e) {
  if (BlobError.is(e) && e.code === "not_found") return null
  throw e
}
```

```python Python
try:
    return bucket.info(path)
except BlobError as e:
    if e.code == "not_found":
        return None
    raise
```

```go Go
info, err := bucket.Info(ctx, path)
if blob.HasCode(err, blob.CodeNotFound) {
	return nil, nil
}
return info, err
```
</CodeGroup>

Every failure the SDK reports is a `BlobError`. Most bad options and arguments are a `TypeError` instead, a `ValueError` in Python, and in Go an error returned where the option is written; see [TypeScript](/blob/sdks/ts/errors#bad-options-are-a-typeerror), [Python](/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror), [Go](/blob/sdks/go/errors#bad-options-are-returned-where-they-are-written). It carries a `code` from a fixed list, a `status`, and a `message` you can show to a user. The same codes are used by every server SDK, in the browser, and inside the React hooks, so an error raised on your server reaches the browser with its code intact.

Catching it and reading its fields: [TypeScript](/blob/sdks/ts/errors), [Python](/blob/sdks/py/errors), [Go](/blob/sdks/go/errors).

---

## The codes

The status is what an upload route answers with.

| Code | Status | Default message | When you see it |
| ---- | ------ | --------------- | --------------- |
| `not_found` | 404 | `not found` | `get`, `info` or `copy` on a path that is not there. Storage answered `NoSuchKey` or `NoSuchUpload`. An unknown upload route. A completion whose object never landed. |
| `already_exists` | 409 | `already exists` | `put` with overwrites refused against a path that already has an object. Carries the existing etag and size. |
| `conflict` | 409 | `the object changed since it was read` | An if-unchanged etag did not match, or storage answered `PreconditionFailed`. Also a [JSON update](/blob/bucket/writing#updating-json) giving up after its maximum attempts. |
| `content_type_not_allowed` | 400 | `content type not allowed` | The declared type is not in the allowed content types, or the file's leading bytes contradict the declaration. |
| `invalid_input` | 400 | `invalid input` | Arguments the SDK will not accept: metadata outside printable ASCII, a malformed upload request body, `input` that fails the route's schema, a delete target that is none of the three shapes (in Python, both or neither of paths and `prefix=`), a signed URL for a path with an empty segment. Browser `upload()` or a hook's `start` where there is no `XMLHttpRequest`, such as a Node script. |
| `too_large` | 413 | `too large` | Over the max size, over the route's constraints, or over what a single PUT can carry when multipart is off and the body needs parts. Also a 413 from storage or from your platform. |
| `empty_body` | 400 | `empty body` | A zero-byte file at `begin`, or a `put` from an incoming request with no body left to read. |
| `length_required` | 411 | `length required` | `put` of an unknown-length stream with neither a size nor a max size. |
| `signature_mismatch` | 403 | `signature mismatch` | A 403 from storage that is not a credential problem: the body length or type does not match what was signed. Also a completion where the stored size is not the declared size. |
| `unauthorized` | 401 | `unauthorized` | The bucket token was rejected, or your own auth check refused the upload. |
| `forbidden` | 403 | `forbidden` | A completion token that is not valid for this route, or has expired. Also a signed URL Upstash refused for the bucket, such as an upload URL on a read-only bucket; the error's cause says why (`reason` in Python). |
| `rate_limited` | 429 | `rate limited` | Storage answered `SlowDown` or `TooManyRequests`, or credential or signing requests are being rate limited. |
| `mint_backoff` | 429 | `the credential service asked for a backoff longer than a request can wait` | The credential service asked for more than 10 seconds of backoff. The retry-after detail says how long. |
| `not_ready` | 503 | `bucket is not ready` | The bucket is not ready to serve requests yet. |
| `partial_delete` | 500 | `some paths were not deleted` | A list or prefix delete where some objects survived. The failed paths list them. |
| `move_left_a_copy` | 500 | `move left a copy at the source` | `move` copied the object but could not delete the source. The destination is kept. |
| `invalid_content_type_pattern` | 500 | `invalid content type pattern` | A content types entry that is not a `type/subtype` or one of `image/*`, `video/*`, `audio/*`. An empty list throws this too. |
| `request_failed` | 500 | `request failed` | Everything else. This is the one code whose status the thrower sets, so it also carries 502 and 503. |

Bad option values are not in this list. An unparseable `'5mbit'`, a missing token, a route with no before-upload callback: those throw where the option is written, at startup, not as a `BlobError` per request. What they throw in each SDK: [TypeScript](/blob/sdks/ts/errors#bad-options-are-a-typeerror), [Python](/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror), [Go](/blob/sdks/go/errors#bad-options-are-returned-where-they-are-written).

---

## Extra fields

Some codes carry extra details. Their names and types in each SDK: [TypeScript](/blob/sdks/ts/errors#fields), [Python](/blob/sdks/py/errors#fields), [Go](/blob/sdks/go/errors#fields).

| Detail | Set by |
| ------ | ------ |
| Hint | Any code. `signature_mismatch` and `length_required` have a built-in one, and many call sites add their own. |
| Failed paths | `partial_delete`: the paths that survived the delete. |
| Etag | `already_exists`: the etag of what is already there. |
| Size | `already_exists`: the size of what is already there. |
| Retry after | `mint_backoff`: seconds the credential service asked the caller to wait. On another code, only when you set it yourself. |
| Cause | The underlying error, when there was one. |

---

## Messages

<CodeGroup>
```ts TypeScript
new BlobError("not_found").message                         // 'Not found'
new BlobError("forbidden", "not your thread").message       // 'Not your thread'
new BlobError("too_large", "cat.png is 3.1 MB, over the 2 MB limit").message
// 'cat.png is 3.1 MB, over the 2 MB limit'
```

```python Python
BlobError("not_found").message                    # 'Not found'
BlobError("forbidden", "not your thread").message  # 'Not your thread'
BlobError("too_large", "cat.png is 3.1 MB, over the 2 MB limit").message
# 'cat.png is 3.1 MB, over the 2 MB limit'
```

```go Go
blob.NewError(blob.CodeNotFound, blob.ErrorOptions{}).Message
// "Not found"

blob.NewError(blob.CodeForbidden, blob.ErrorOptions{
	Message: "not your thread",
}).Message
// "Not your thread"

blob.NewError(blob.CodeTooLarge, blob.ErrorOptions{
	Message: "cat.png is 3.1 MB, over the 2 MB limit",
}).Message
// "cat.png is 3.1 MB, over the 2 MB limit"
```
</CodeGroup>

Messages are sentence-cased, so an app can print the message directly. A message that opens with an identifier, like a MIME type or a file name, keeps that identifier's case.

A message never carries a credential, a token, or an internal path. Every message is assembled from a code, a caller-supplied string, an HTTP status, or the reason Upstash's signing service gave. A suspended or read-only bucket is the exception: that notice is meant for you, not your users, so it is on the error's cause instead.

### Hints fold into the message

<CodeGroup>
```ts TypeScript
new BlobError("length_required").message
// 'Length required (pass { size } or { maxSize } so the length is known before the first byte)'
```

```python Python
BlobError("length_required").message
# 'Length required (pass size= or max_size= so the length is known before the first byte)'
```

```go Go
blob.NewError(blob.CodeLengthRequired, blob.ErrorOptions{}).Message
// "Length required (pass Size or MaxSize so the length is known
// before the first byte)"
```
</CodeGroup>

A hint is appended to the message in parentheses, so printing the message alone is enough. The hint is still there separately if you want to lay it out yourself. A message that already contains its hint is not doubled.

`signature_mismatch` and `length_required` have a built-in hint. Its text in each SDK: [TypeScript](/blob/sdks/ts/errors#built-in-hints), [Python](/blob/sdks/py/errors#built-in-hints), [Go](/blob/sdks/go/errors#built-in-hints).

---

## Errors in the browser

```tsx app/picker.tsx
"use client"
import type { BlobError } from "@upstash/blob/react"
import { useUpload } from "@/lib/upload-hooks"

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

  return (
    <>
      <input
        type="file"
        accept={accept}
        onChange={(e) => start({ file: e.target.files?.[0] })}
      />
      {upload?.status === "error" && <p role="alert">{describe(upload.error)}</p>}
    </>
  )
}

function describe(error: BlobError): string {
  switch (error.code) {
    case "unauthorized":
      return "Your session expired. Sign in and try again."
    case "rate_limited":
      return `Too many uploads. Try again in ${error.retryAfter ?? 30}s.`
    case "not_ready":
      return "Storage is warming up. Try again in a moment."
    default:
      // too_large, content_type_not_allowed and the rest already read as a sentence.
      return error.message
  }
}
```

An upload route, in any server SDK, answers every refusal with the error's own code and status, and the browser rebuilds it. `error.code` inside a hook is the code your server raised, not a status number you have to decode.

### What reaches the browser, in order

A route runs its [error callback](/blob/uploads/upload-handler#handling-errors) first. If it returns a response, that is the answer; if it returns a `BlobError`, the answer is that error's JSON at its own status. Otherwise the throw falls through three cases:

1. **A `BlobError` is answered as itself**, at its own status, with its hint, failed paths, etag, size and retry-after when they are set.
2. **An app error carrying a `status` between 400 and 599 is mapped through the table below.** This is how an auth check that throws its own 401 reaches the browser as `unauthorized`.
3. **Anything else is treated as your bug and rethrown**, so your framework logs it with its stack rather than masking it as a generic 500.

In Go the status in case 2 comes from a `StatusCode() int` method on the error, and case 3 has nothing to rethrow to: the handler logs the error on its `Logger` and answers a 500 with no detail. See [Refusing](/blob/sdks/go/upload-handler#refusing).

| Status | Code |
| ------ | ---- |
| 401 | `unauthorized` |
| 403 | `forbidden` |
| 404 | `not_found` |
| 409 | `conflict` |
| 411 | `length_required` |
| 413 | `too_large` |
| 429 | `rate_limited` |

Any other status becomes `request_failed`, keeping the status it arrived with.

A throw out of the completion callback also deletes the completed object before the error is answered. See [Recording the upload](/blob/uploads/upload-handler#recording-the-upload).

---

## What storage errors map to

Storage is Cloudflare R2. Its errors are normalized before they leave the SDK, first matching wins.

| Storage answered | Becomes |
| ---------------- | ------- |
| 404, or `NoSuchKey` / `NoSuchUpload` | `not_found` |
| 412, or `PreconditionFailed` | `conflict` |
| 401 | `unauthorized` |
| 403 with `ExpiredToken`, `InvalidAccessKeyId` or `TokenRefreshRequired` | `unauthorized`, "storage refused the temporary credential", hinting that it expired mid-request and the SDK re-mints and retries once |
| any other 403 | `signature_mismatch` |
| 429, or `SlowDown` / `TooManyRequests` | `rate_limited`, "storage rate limited the request" |
| 503 | `not_ready` |
| 413, or `EntityTooLarge` | `too_large` |
| anything else | `request_failed`, message `storage responded <status> <Code>`, with storage's own `<Message>` on the error's cause |

For that last row the status is passed through, except that a 5xx becomes 502, since the failure is upstream of your app rather than in it.

---

## Errors the browser raises on its own

Some failures never reach your route, so the browser names them itself.

**A PUT that fails with no status and no bytes sent** was refused by the browser before it went out, and the reason is never visible to script because it is the preflight that failed:

```
the browser blocked the request before sending any bytes, which is almost always CORS:
the bucket has to allow PUT and the signed headers from this origin
```

Buckets allow every origin by default, so this only shows up on a bucket whose CORS policy was narrowed.

**A 403 on a freshly minted presign** becomes `signature_mismatch`. A 401 or 403 on an older URL is read as an expired signature and the browser asks the route for a new one instead. See [Retries](/blob/uploads/large-files#retries).

**Exhausted retries** become `request_failed`, carrying the attempt count and the last status, hinted with what to do next:

```
Upload failed after 8 attempts (last status 500) (the parts that landed are kept:
task.retry(), or pick the same file again)
```

**A canceled upload rejects with an `AbortError`, not a `BlobError`.** The record's status is `canceled` and it carries no `error` at all, so a cancel never renders as a failure.

```ts
const record = start({ file })
record?.cancel()          // status becomes 'canceled', error stays undefined
```

---

## Platform body limits

```tsx
const { start, upload } = useServerUpload("/api/avatar")

if (upload?.status === "error" && upload.error.code === "too_large") {
  // Either your own maxSize or the platform's body cap. e.hint says which.
}
```

This applies to `useServerUpload` and any route of your own that the bytes pass through. It does not apply to direct browser uploads.

Vercel caps a serverless request body at 4.5MB, AWS Lambda at 6MB, and Cloudflare at 100MB on the free plan. A file over that cap is refused with a 413 that never reached your route, so it carries no code of its own. The SDK turns it into `too_large` and attaches the cap as a hint:

```
Too large (Vercel caps a serverless request body at 4.5MB, AWS Lambda at 6MB,
Cloudflare at 100MB on the free plan)
```

Keep a proxied route's own max size under the platform's cap, so the refusal comes from your code with your wording. A file bigger than the cap needs a direct browser upload instead.

---

## Credential errors

<CodeGroup>
```ts TypeScript
try {
  await bucket.put("u/7/report.pdf", body)
} catch (e) {
  if (BlobError.is(e) && e.code === "mint_backoff") {
    return retryAfterSeconds(e.retryAfter ?? 10)
  }
  throw e
}
```

```python Python
try:
    bucket.put("u/7/report.pdf", body)
except BlobError as e:
    if e.code == "mint_backoff":
        return retry_after_seconds(e.retry_after or 10)
    raise
```

```go Go
_, err := bucket.Put(ctx, "u/7/report.pdf", body, nil)
var be *blob.Error
if errors.As(err, &be) && be.Code == blob.CodeMintBackoff {
	return retryAfter(be.RetryAfter)
}
return err
```
</CodeGroup>

Three codes come from the credential service rather than from storage or from your code.

| Code | Status | Meaning | What to do |
| ---- | ------ | ------- | ---------- |
| `unauthorized` | 401 | The bucket token was rejected, or the bucket is suspended. | Check `UPSTASH_BLOB_TOKEN`. When a signed URL was refused, the error's cause says why (`reason` in Python). Nothing retries this. |
| `not_ready` | 503 | The bucket is not ready yet. | Retry the request. |
| `mint_backoff` | 429 | The service asked for a backoff longer than a request can wait, over 10 seconds. The retry-after detail says how long. | Retry the request later rather than blocking on it. |

The SDK waits out short backoffs itself. `mint_backoff` is a pause too long for one request to wait through, handed back to you instead of holding a serverless invocation open.

Credentials are short-lived and re-minted before they expire. One that expires mid-request is handled inside the SDK, so only a second refusal surfaces. See [How signing works](/blob/reference/signing).
