Skip to main content
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.
uploads.go
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

main.go
*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:
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. The handler reads the JSON body of each POST, at most 1 MiB; the browser client’s bodies are a few kilobytes.

Options

uploads.go
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

uploads.go
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

uploads.go
BeforeUploadArgs[C]: It returns a BeforeUploadResult: 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

uploads.go
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

uploads.go
UploadCompleteArgs[C] embeds a CompletedBlob, so the stored object’s fields are on it directly: 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

uploads.go
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

uploads.go
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

uploads.go
app/page.tsx
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

uploads.go
app/page.tsx
Input and State work on any UploadRoute, and on the handler itself.
  • Input gets the raw JSON the browser sent as input, nil when it sent none, and returns the parsed value or an error. Its result is a.Input, an any to assert back to your type.
  • blob.JSONInput[T]() is an Input that decodes into a T with encoding/json and refuses a missing input. Decoding checks shape, not values: validate fields yourself, in Input or in OnBeforeUpload.
  • An error from Input is refused with invalid_input, its Error() text as the message. Return a *blob.Error to pick the code and message yourself.
  • State crosses the browser as JSON, and comes back as the raw json.RawMessage to decode into whatever type you like. A value encoding/json cannot 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.