NewUploadHandler builds the Go 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 Go names, types and net/http wiring.
The type parameter is what Context returns, and it is a.Ctx in every callback, typed. With no Context, use any type, such as struct{}, and a.Ctx is its zero value.
OnBeforeUpload, OnUploadComplete and OnError each take one argument, a struct named after the callback. The handler is built once, at startup; bad options are returned as an error there.
Mounting it#
*UploadHandler is an http.Handler. It answers GET and POST at one URL, so mount it on the path for every method rather than behind a pattern such as POST /api/upload. Any other method is a 405. It works under any router that takes an http.Handler: chi’s r.Handle, echo.WrapHandler, gin.WrapH.
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:
Route names and upload.blob.data are not checked at compile time. See With a server in another language.
The handler reads the JSON body of each POST, at most 1 MiB; the browser client’s bodies are a few kilobytes.
Options#
| Field | Type | Description |
|---|---|---|
Bucket | *Bucket | The bucket every route writes to. See The bucket. |
ContentTypes | []string | What the routes accept. See Content types. |
MaxSize | int64 | The largest file the routes accept, in bytes. Zero is no limit. See Max size. |
Multipart | blob.Multipart | Where an upload stops being one PUT. See Large files. |
Endpoint | string | Where the handler is mounted. Only needed to separate two handlers on one bucket. |
Context | func(*http.Request) (C, error) | Runs once per POST. Its value is a.Ctx in every callback. See Context. |
Input | func(json.RawMessage) (any, error) | Validates what the browser sends as input before OnBeforeUpload runs. See Input and state. |
OnBeforeUpload | func(BeforeUploadArgs[C]) (BeforeUploadResult, error) | Required, here or on every route. See OnBeforeUpload. |
OnUploadComplete | func(UploadCompleteArgs[C]) (any, error) | What it returns becomes upload.blob.data. See OnUploadComplete. |
OnError | func(UploadErrorArgs[C]) error | Sees every refusal except a 405 and a completion result that cannot be encoded. See OnError. |
Routes | map[string]UploadRoute[C] | Mounts several routes at this one endpoint. See Routes. |
Logger | *slog.Logger | Receives what a response cannot carry: an object a refusal could not delete, an error answered as a 500, a CORS warning. Nil is slog.Default(). |
There is no constraints struct: ContentTypes and MaxSize are fields of their own, on the handler, on a route, and on what OnBeforeUpload returns.
Everything except Routes, Endpoint, Context and Logger is a default each route inherits; see Multiple routes. NewUploadHandler returns an error for a bad option: an invalid route name, an empty non-nil Routes, a missing OnBeforeUpload, a bad ContentTypes entry, a negative MaxSize, or a missing token. See Bad options.
The bucket#
What the handler uses with no Bucket is on Handler options. Routes that fall back to UPSTASH_BLOB_TOKEN share one bucket. Pass Bucket when the token lives under another variable or the bucket needs Cache.
OnBeforeUpload#
BeforeUploadArgs[C]:
| Field | Type | Description |
|---|---|---|
Ctx | C | Whatever Context returned for this request. |
Route | string | The route this file was sent to. "" when the handler mounts no named routes. |
Request | *http.Request | The begin request, headers and cookies intact. |
File | UploadFile | What the browser declared, before a byte was sent: Name, Type and Size. |
Input | any | What the route’s Input returned, or nil. |
It returns a BeforeUploadResult:
| Field | Type | Description |
|---|---|---|
Path | string | Required. Where the object is stored. |
Cache | string | The Cache-Control this object is stored with. See Caching. |
Metadata | map[string]string | Signed into the upload and handed back to OnUploadComplete. Keys must be lowercase, as on Put. |
ContentTypes | []string | Narrows this one upload’s allowed types. |
MaxSize | int64 | Narrows this one upload’s limit. |
State | any | Anything computed here that OnUploadComplete and OnError should get without a lookup. It is encoded with encoding/json. See Input and state. |
What each field does is on Authorizing an upload. A path with a . or .. segment, an empty segment (a//b, a leading /) or a control character is refused with invalid_input, since a filename from the browser can put one there.
Refusing#
Return a *blob.Error to refuse with its code. Any error that is or wraps one works the same. An error with a StatusCode() int method between 400 and 599 answers with that status, mapped to a code through the table on What reaches the browser, with its Error() text as the message:
Any other error is logged on Logger and answered as a 500 with no detail.
OnUploadComplete#
UploadCompleteArgs[C] embeds a CompletedBlob, so the stored object’s fields are on it directly:
| Field | Type | Description |
|---|---|---|
Path | string | Where the object is stored. |
URL | string | The public URL. "" on a private bucket. |
VersionedURL | string | URL with the etag on the query. |
Size | int64 | Bytes actually stored. |
ETag | string | The stored object’s etag. |
UploadedAt | time.Time | When storage wrote it. |
ContentType | string | What the object is stored as. |
Ctx | C | What Context returned for this request. |
Route | string | The route name, "" for a sole route. |
Request | *http.Request | The end request. |
File | UploadFile | What the browser declared at begin. |
UploadID | string | Identifies this upload. Stable across retries. |
MultipartUploadID | string | For AbortMultipartUpload. "" for a single PUT. |
Metadata | map[string]string | What OnBeforeUpload returned, minus the SDK’s marker. |
State | json.RawMessage | What OnBeforeUpload returned as State, as JSON. Nil when it returned none. |
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 encoded with encoding/json and handed to the browser as upload.blob.data. Return nil and data is not set. A value that does not encode is logged and answered as a 500, and unlike an error from the callback itself, it does not delete the object.
During end, a.Request.Context() is not canceled when the browser disconnects, so a database write made with it runs to completion rather than leave a completed object unrecorded.
OnError#
| Field | Type | Description |
|---|---|---|
Ctx | C | The zero value when the request failed before Context returned: no route matched, or Context itself failed. |
Route | string | The name from the query, even when nothing mounts it. |
Request | *http.Request | The request. |
Err | error | Whatever was returned. |
File | *UploadFile | As much as the request had reached before it failed, else nil. |
Path, Metadata, State | As much as the request had reached, else their zero values. |
Return a *blob.Error, or an error that wraps one, to answer with it. Return nil, or any other error, to leave the answer alone. See Handling errors.
Context#
Context takes the request. What it returns is a.Ctx in every callback. An error it returns refuses the request under the rules in Refusing. When it runs is on Context.
Routes#
UploadRoute[C] has the fields Bucket, ContentTypes, MaxSize, Multipart, Input, OnBeforeUpload, OnUploadComplete and OnError. A field it sets overrides the handler’s, and one it leaves at its zero value inherits it. Since a zero value means “inherit”, clearing a limit the handler set takes a value of its own: ContentTypes: []string{} clears the list, and MaxSize: blob.NoLimit clears the size. The rules for names and inheritance are on Multiple routes.
Input and state#
Input and State work on any UploadRoute, and on the handler itself.
Inputgets the raw JSON the browser sent asinput, nil when it sent none, and returns the parsed value or an error. Its result isa.Input, ananyto assert back to your type.blob.JSONInput[T]()is anInputthat decodes into aTwithencoding/jsonand refuses a missinginput. Decoding checks shape, not values: validate fields yourself, inInputor inOnBeforeUpload.- An error from
Inputis refused withinvalid_input, itsError()text as the message. Return a*blob.Errorto pick the code and message yourself. Statecrosses the browser as JSON, and comes back as the rawjson.RawMessageto decode into whatever type you like. A valueencoding/jsoncannot encode is answered as a 500.
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 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 on each route, again only if storage did not answer. The TypeScript handler has no such check. Buckets allow every origin by default; see Errors the browser raises on its own.