Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions blob/recipes/fastapi-react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A React app whose backend is Python: users pick files, the bytes go from the bro
Three choices make that work:

- **An `AsyncUploadHandler` mounted on FastAPI.** It speaks the same protocol as the TypeScript handler, so `useUpload` from `@upstash/blob/react` works against it unchanged.
- **One auth check for every route.** `require_user` is both the handler's `context` and the FastAPI dependency on the other endpoints, and `owner` binds each upload to the user who began it.
- **One auth check for every route.** `require_user` is both the handler's `context` and the FastAPI dependency on the other endpoints.
- **One origin.** Vite proxies `/api` to FastAPI in development, and FastAPI serves the built app in production, so the session cookie reaches the handler with no CORS setup.

This recipe uses a public bucket. If you have not created one yet, start with the [Quickstart](/blob/overall/quickstart).
Expand Down Expand Up @@ -107,15 +107,14 @@ uploads = AsyncUploadHandler(
content_types=["image/*", "application/pdf"],
max_size="25mb",
context=require_user,
owner=lambda user: user.id,
on_before_upload=authorize,
on_upload_complete=record,
)
```

The handler takes images and PDFs up to 25 MB and refuses anything else before a byte is uploaded. It runs on FastAPI's event loop, so it is an `AsyncUploadHandler` over an `AsyncBucket`; see [Sync or async](/blob/sdks/py/upload-handler#sync-or-async).

`require_user` runs on every POST to the route. Its `HTTPException(401)` reaches the browser as a `BlobError` with code `unauthorized`, so the React side can tell a dead session from a refused file. `owner` signs the user id into each upload, so a second signed-in user holding the completion token cannot finish or cancel it. See [context](/blob/sdks/py/upload-handler#context) and [owner](/blob/sdks/py/upload-handler#owner).
`require_user` runs on every POST to the route. Its `HTTPException(401)` reaches the browser as a `BlobError` with code `unauthorized`, so the React side can tell a dead session from a refused file. See [context](/blob/sdks/py/upload-handler#context).

`unique_path` adds a random suffix to the filename, so two uploads of `photo.png` are two objects. `args.file.name` is the original filename, and the completion callback is the only place it survives, so the row stores it.

Expand Down Expand Up @@ -303,7 +302,7 @@ if __name__ == "__main__":

<CardGroup cols={2}>
<Card title="Python upload handler" href="/blob/sdks/py/upload-handler">
Options, callbacks, routes, `input` and `owner`.
Options, callbacks, routes and `input`.
</Card>

<Card title="Upload client" href="/blob/uploads/upload-client">
Expand Down
21 changes: 1 addition & 20 deletions blob/sdks/py/upload-handler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ uploads = AsyncUploadHandler(
multipart="100mb",
endpoint="/api/upload",
context=require_user,
owner=lambda user: user.id,
input=Thread,
on_before_upload=authorize,
on_upload_complete=record,
Expand All @@ -202,7 +201,6 @@ uploads = AsyncUploadHandler(
| `multipart` | `MultipartOption` | Where an upload stops being one PUT. See [Large files](/blob/uploads/large-files#changing-the-threshold). |
| `endpoint` | `str` | Where the handler is mounted. Only needed to separate two handlers on one bucket. |
| `context` | `Callable[[request], Any]` | Runs once per POST. Its value is `args.ctx` in every callback. Without it, `ctx` is `None`. See [context](#context). |
| `owner` | `Callable[[ctx], Any]` | Binds each upload to who began it. Python only. See [owner](#owner). |
| `input` | pydantic model, `TypeAdapter`, or callable | Validates what the browser sends as `input` before `on_before_upload` runs. Every route inherits it. See [Input and state](#input-and-state). |
| `on_before_upload` | `Callable[[BeforeUploadArgs], BeforeUploadResult]` | Required, here or on every route. See [on_before_upload](#on_before_upload). |
| `on_upload_complete` | `Callable[[UploadCompleteArgs], Any]` | What it returns becomes `upload.blob.data`. See [on_upload_complete](#on_upload_complete). |
Expand All @@ -211,7 +209,7 @@ uploads = AsyncUploadHandler(

There is no `constraints` object: `content_types` and `max_size` are options of their own, on the handler, on a route, and on what `on_before_upload` returns.

Everything except `routes`, `endpoint`, `context` and `owner` is a default each route inherits; see [Multiple routes](/blob/uploads/upload-handler#multiple-routes). Bad options raise where they are written, at startup: a `ValueError` for an unparseable size, a `TypeError` for an invalid route name, an empty `routes`, a missing `on_before_upload`, or a missing token. See [Bad options](/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror).
Everything except `routes`, `endpoint` and `context` is a default each route inherits; see [Multiple routes](/blob/uploads/upload-handler#multiple-routes). Bad options raise where they are written, at startup: a `ValueError` for an unparseable size, a `TypeError` for an invalid route name, an empty `routes`, a missing `on_before_upload`, or a missing token. See [Bad options](/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror).

### The bucket

Expand Down Expand Up @@ -382,23 +380,6 @@ uploads = AsyncUploadHandler(

`context` takes the framework's request. Its return value, awaited on `AsyncUploadHandler`, is `args.ctx` in every callback. When it runs is on [Context](/blob/uploads/upload-handler#context).

### owner

```python app/uploads.py
uploads = AsyncUploadHandler(
context=require_user,
owner=lambda user: user.id,
on_before_upload=authorize,
)
```

`owner` reads `ctx` and returns who the upload belongs to. Its value, as a string, is signed into the [completion token](/blob/reference/signing#the-completion-token) at `begin`. Every later request for that upload, `parts`, `end` and `cancel`, must produce the same owner or it is refused with `forbidden` before storage or any callback is touched. So one signed-in user cannot finish, re-sign or cancel an upload somebody else began, even holding its token.

- `owner` needs `context`. Without one it raises a `TypeError` at startup.
- A return of `None` or `""` is `unauthorized`. A `bool` raises a `TypeError`, since `True` would make every user the same owner.

The TypeScript handler has no `owner`.

---

## Routes
Expand Down
21 changes: 1 addition & 20 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6917,7 +6917,6 @@ uploads = AsyncUploadHandler(
multipart="100mb",
endpoint="/api/upload",
context=require_user,
owner=lambda user: user.id,
input=Thread,
on_before_upload=authorize,
on_upload_complete=record,
Expand All @@ -6934,7 +6933,6 @@ uploads = AsyncUploadHandler(
| `multipart` | `MultipartOption` | Where an upload stops being one PUT. See [Large files](/docs/blob/uploads/large-files#changing-the-threshold). |
| `endpoint` | `str` | Where the handler is mounted. Only needed to separate two handlers on one bucket. |
| `context` | `Callable[[request], Any]` | Runs once per POST. Its value is `args.ctx` in every callback. Without it, `ctx` is `None`. See [context](#context). |
| `owner` | `Callable[[ctx], Any]` | Binds each upload to who began it. Python only. See [owner](#owner). |
| `input` | pydantic model, `TypeAdapter`, or callable | Validates what the browser sends as `input` before `on_before_upload` runs. Every route inherits it. See [Input and state](#input-and-state). |
| `on_before_upload` | `Callable[[BeforeUploadArgs], BeforeUploadResult]` | Required, here or on every route. See [on_before_upload](#on_before_upload). |
| `on_upload_complete` | `Callable[[UploadCompleteArgs], Any]` | What it returns becomes `upload.blob.data`. See [on_upload_complete](#on_upload_complete). |
Expand All @@ -6943,7 +6941,7 @@ uploads = AsyncUploadHandler(

There is no `constraints` object: `content_types` and `max_size` are options of their own, on the handler, on a route, and on what `on_before_upload` returns.

Everything except `routes`, `endpoint`, `context` and `owner` is a default each route inherits; see [Multiple routes](/docs/blob/uploads/upload-handler#multiple-routes). Bad options raise where they are written, at startup: a `ValueError` for an unparseable size, a `TypeError` for an invalid route name, an empty `routes`, a missing `on_before_upload`, or a missing token. See [Bad options](/docs/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror).
Everything except `routes`, `endpoint` and `context` is a default each route inherits; see [Multiple routes](/docs/blob/uploads/upload-handler#multiple-routes). Bad options raise where they are written, at startup: a `ValueError` for an unparseable size, a `TypeError` for an invalid route name, an empty `routes`, a missing `on_before_upload`, or a missing token. See [Bad options](/docs/blob/sdks/py/errors#bad-options-raise-typeerror-or-valueerror).

### The bucket

Expand Down Expand Up @@ -7112,23 +7110,6 @@ uploads = AsyncUploadHandler(

`context` takes the framework's request. Its return value, awaited on `AsyncUploadHandler`, is `args.ctx` in every callback. When it runs is on [Context](/docs/blob/uploads/upload-handler#context).

### owner

```python app/uploads.py
uploads = AsyncUploadHandler(
context=require_user,
owner=lambda user: user.id,
on_before_upload=authorize,
)
```

`owner` reads `ctx` and returns who the upload belongs to. Its value, as a string, is signed into the [completion token](/docs/blob/reference/signing#the-completion-token) at `begin`. Every later request for that upload, `parts`, `end` and `cancel`, must produce the same owner or it is refused with `forbidden` before storage or any callback is touched. So one signed-in user cannot finish, re-sign or cancel an upload somebody else began, even holding its token.

* `owner` needs `context`. Without one it raises a `TypeError` at startup.
* A return of `None` or `""` is `unauthorized`. A `bool` raises a `TypeError`, since `True` would make every user the same owner.

The TypeScript handler has no `owner`.

***

## Routes
Expand Down