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
FieldTypeDescription
Bucket*BucketThe bucket every route writes to. See The bucket.
ContentTypes[]stringWhat the routes accept. See Content types.
MaxSizeint64The largest file the routes accept, in bytes. Zero is no limit. See Max size.
Multipartblob.MultipartWhere an upload stops being one PUT. See Large files.
EndpointstringWhere the handler is mounted. Only needed to separate two handlers on one bucket.
Contextfunc(*http.Request) (C, error)Runs once per POST. Its value is a.Ctx in every callback. See Context.
Inputfunc(json.RawMessage) (any, error)Validates what the browser sends as input before OnBeforeUpload runs. See Input and state.
OnBeforeUploadfunc(BeforeUploadArgs[C]) (BeforeUploadResult, error)Required, here or on every route. See OnBeforeUpload.
OnUploadCompletefunc(UploadCompleteArgs[C]) (any, error)What it returns becomes upload.blob.data. See OnUploadComplete.
OnErrorfunc(UploadErrorArgs[C]) errorSees every refusal except a 405 and a completion result that cannot be encoded. See OnError.
Routesmap[string]UploadRoute[C]Mounts several routes at this one endpoint. See Routes.
Logger*slog.LoggerReceives 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#

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]:

FieldTypeDescription
CtxCWhatever Context returned for this request.
RoutestringThe route this file was sent to. "" when the handler mounts no named routes.
Request*http.RequestThe begin request, headers and cookies intact.
FileUploadFileWhat the browser declared, before a byte was sent: Name, Type and Size.
InputanyWhat the route’s Input returned, or nil.

It returns a BeforeUploadResult:

FieldTypeDescription
PathstringRequired. Where the object is stored.
CachestringThe Cache-Control this object is stored with. See Caching.
Metadatamap[string]stringSigned into the upload and handed back to OnUploadComplete. Keys must be lowercase, as on Put.
ContentTypes[]stringNarrows this one upload’s allowed types.
MaxSizeint64Narrows this one upload’s limit.
StateanyAnything 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#

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:

FieldTypeDescription
PathstringWhere the object is stored.
URLstringThe public URL. "" on a private bucket.
VersionedURLstringURL with the etag on the query.
Sizeint64Bytes actually stored.
ETagstringThe stored object’s etag.
UploadedAttime.TimeWhen storage wrote it.
ContentTypestringWhat the object is stored as.
CtxCWhat Context returned for this request.
RoutestringThe route name, "" for a sole route.
Request*http.RequestThe end request.
FileUploadFileWhat the browser declared at begin.
UploadIDstringIdentifies this upload. Stable across retries.
MultipartUploadIDstringFor AbortMultipartUpload. "" for a single PUT.
Metadatamap[string]stringWhat OnBeforeUpload returned, minus the SDK’s marker.
Statejson.RawMessageWhat 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#

uploads.go
FieldTypeDescription
CtxCThe zero value when the request failed before Context returned: no route matched, or Context itself failed.
RoutestringThe name from the query, even when nothing mounts it.
Request*http.RequestThe request.
ErrerrorWhatever was returned.
File*UploadFileAs much as the request had reached before it failed, else nil.
Path, Metadata, StateAs 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#

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.