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
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
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
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
*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:
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.
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.
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 onLogger 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.