Skip to main content
UploadHandler and AsyncUploadHandler are the Python server side of a direct browser upload. What the handler does, the order its callbacks run in, and their guarantees are on Upload handler. This page has the Python names, types and framework wiring.
app/uploads.py
on_before_upload, on_upload_complete and on_error each take one argument, a frozen dataclass named after the callback, and every option is a keyword. The handler is built once, at import; bad options raise there.

Mounting it

upload_view(uploads) returns a view that answers GET and POST at one URL. args.request in every callback is the framework’s own request: a Starlette Request, a Django HttpRequest, or Flask’s request. The wrong handler for a module raises a TypeError when the view is built. The browser side is @upstash/blob/react, unchanged. There is no TypeScript type to bind the hooks to, so call uploadHooks() without a type parameter:
lib/upload-hooks.ts
app/page.tsx
Route names and upload.blob.data are not checked at compile time. See With a server in another language, and FastAPI + React for a whole app.

Other frameworks

app/main.py
handle() is what the adapters call. It takes the method, the framework’s request (handed to the callbacks as request), the route query parameter, the raw body and the headers, and returns an UploadResponse with status, headers and body: bytes. On AsyncUploadHandler it is awaited.

Sync or async

An UploadHandler callback that returns a coroutine raises a TypeError, since nothing would await it. The two take the same options. The Flask version of the handler above:
app/uploads.py

Options

app/uploads.py
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 and context is a default each route inherits; see 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.

The bucket

app/uploads.py
What the handler uses with no bucket is on Handler options. Pass bucket= when the token lives under another variable or the bucket needs cache. An UploadHandler takes a Bucket and an AsyncUploadHandler an AsyncBucket; the other kind raises a TypeError.

on_before_upload

app/uploads.py
BeforeUploadArgs: It returns a BeforeUploadResult. Anything else raises a TypeError while the request runs. What each field does is on Authorizing an upload. A path with a . or .. segment is refused with invalid_input, since a filename from the browser can put one there.

Refusing

app/uploads.py
Raise a BlobError to refuse. A framework’s own HTTP exception works too, mapped through the status table on What reaches the browser: the status is read from status_code (FastAPI, Starlette), code (Werkzeug, so Flask’s abort(401)) or status, and the message from detail, description or message.
app/uploads.py
Any other exception is re-raised, so your framework logs it and answers 500.

Callbacks in another file

A callback is a function, so it lives wherever you import it from. BeforeUploadArgs, BeforeUploadResult, UploadCompleteArgs, UploadErrorArgs and FileInfo are exported from upstash_blob to annotate it. ctx, input and state are typed Any; narrow them yourself.

on_upload_complete

app/uploads.py
UploadCompleteArgs is a CompletedBlob with the upload’s facts added: What each field means, and why it may run more than once or not at all, is on Recording the upload. What it returns is handed to the browser as upload.blob.data. It must be JSON: a dict, list, str, number, bool or None, or a pydantic model, a dataclass or a datetime, which are converted. Anything else raises a TypeError once the callback has returned. Unlike a raise from the callback itself, that does not delete the object: it stays stored, and the browser gets a 500. Return None and data is not set.

on_error

app/uploads.py
Return a BlobError, an UploadResponse, or the framework’s own response object to answer with it. Return anything else, None included, to leave the answer alone. See Handling errors.

context

app/uploads.py
context takes the framework’s request. Its return value, awaited on AsyncUploadHandler, is args.ctx in every callback. When it runs is on Context.

Routes

app/uploads.py
app/page.tsx
UploadRoute takes any of bucket, content_types, max_size, multipart, input, on_before_upload, on_upload_complete and on_error. A field it sets overrides the handler’s, and one it leaves out inherits it. content_types=None or max_size=None clears a limit the handler set. A value in routes that is not an UploadRoute raises a TypeError. The rules for names and inheritance are on Multiple routes.

Input and state

app/uploads.py
app/page.tsx
There is no uploadRoute() here: input and state work on a plain UploadRoute, and on the handler itself. pydantic is not a dependency of the SDK; install it to use a model.
  • input takes a pydantic BaseModel subclass, a pydantic TypeAdapter, or any callable that returns the parsed value or raises ValueError. Its result is args.input, so a model arrives as a model instance. A browser that sent no input reaches the validator as None.
  • A pydantic error is refused with invalid_input, its issues joined as loc: msg, so a bad threadId reads ThreadId: Input should be a valid UUID, .... A callable’s ValueError becomes the message.
  • state crosses the browser as JSON, so it comes back as dict, list, str, numbers and None: a UUID or a datetime has to be a string before it goes in. A value json.dumps cannot write raises a TypeError.
How input is validated and what state exposes are on Input and state.

CORS check

The first upload from each page origin also sends storage the CORS preflight the browser is about to send, against the URL it just signed. When storage refuses it, the handler logs a warning on the upstash_blob logger naming the origin to add to the bucket’s CORS settings in the Upstash Console. It runs in the background, never delays or fails the request, and runs once per origin. The TypeScript handler has no such check. Buckets allow every origin by default; see Errors the browser raises on its own.