Constraints are the two limits an upload route enforces before it signs anything: how big a file may be, and what type it may be.
They are checked at begin, from the name, type and size the browser declared, before onBeforeUpload runs. A refused file leaves nothing behind: no presigned URL, no row, no multipart upload.
Omitting constraints accepts any type at any size.
maxSize#
maxSize takes a Size. Sizes are decimal, so '2mb' is 2,000,000 and '5mib' throws. A refusal reads back in the same units:
An unparseable size throws a TypeError where the option is written, at startup, not once per request.
contentTypes#
Each entry is either an exact type/subtype, or one of three wildcards: image/*, video/* and audio/*.
Anything else throws invalid_content_type_pattern: */*, text/*, and strings that are not a media type (png, image/, /png). An empty list throws too; omit the option to accept anything.
What the wildcards expand to#
| Wildcard | Expands to |
|---|---|
image/* | image/png, image/jpeg, image/gif, image/webp, image/bmp, image/tiff, image/avif, image/heic, image/heif, image/x-icon |
video/* | video/mp4, video/quicktime, video/webm, video/x-matroska, video/x-msvideo, video/mpeg, video/ogg, video/3gpp |
audio/* | audio/mpeg, audio/wav, audio/ogg, audio/opus, audio/flac, audio/aac, audio/mp4, audio/webm |
image/* deliberately does not include image/svg+xml. An SVG can contain script, so allow it explicitly by listing 'image/svg+xml'.
Aliases#
Browsers and operating systems send several spellings for some types. Both the declared type and your list are canonicalized before comparison, so contentTypes: ['image/jpg'] accepts a file declared image/jpeg, and the reverse. Parameters are stripped, so image/png; charset=binary is image/png.
Alias table
| Written | Canonicalizes to |
|---|---|
image/jpg | image/jpeg |
image/pjpeg | image/jpeg |
image/vnd.microsoft.icon | image/x-icon |
audio/x-wav | audio/wav |
audio/wave | audio/wav |
audio/vnd.wave | audio/wav |
audio/mp3 | audio/mpeg |
audio/x-flac | audio/flac |
audio/x-aac | audio/aac |
video/avi | video/x-msvideo |
video/msvideo | video/x-msvideo |
application/x-gzip | application/gzip |
application/x-zip-compressed | application/zip |
application/vnd.rar | application/x-rar-compressed |
Byte sniffing#
A route with contentTypes checks more than the declared type. The browser sends the file's leading bytes with the begin request, so a mislabelled file is refused before the upload rather than after it. Two checks run:
- The declared type must be in the allow list. A
report.exedeclared asapplication/x-msdownloadis refused here. - The leading bytes must not clearly contradict the declared type. If the bytes prove nothing, the file passes.
The second check is lenient on purpose so real files are not refused. A .docx really is a zip, and so are .epub, .jar and .apk. Formats that share a signature, like mp4 and heic, or webm and mkv, never contradict a declaration. Neither does application/octet-stream.
This is a convenience, not a security control. The bytes never reach your server, so a client can send an honest sample and then upload something else. It is not malware scanning, and stored objects should still be treated as untrusted.
Per-route constraints#
| Route | maxSize | contentTypes |
|---|---|---|
attachment | 20,000,000, inherited | ['image/png'], inherited |
avatar | 2,000,000, replaced | ['image/png'], inherited |
large | 2,000,000,000, replaced | none, cleared by null |
A route's constraints override the handler's key by key. A key the route does not mention is inherited. null clears a key the handler set.
Narrowing per user#
onBeforeUpload may return constraints to tighten the route's limits for this one upload, once it knows who is uploading. They are checked against the same file right after it returns. Widening throws a TypeError, so a route's code always shows the most it can accept.
In the browser#
GET on the upload route serves the constraints it enforces, cached for 60 seconds, so a deploy that changes a route's limits reaches clients within a minute.
useUpload exposes two things from it. accept is contentTypes joined with commas, ready for an <input accept>. constraints is the served document itself, so a page can show the limit it enforces.
A file over maxSize is refused in the browser before any request is made. It still becomes a record with status: 'error' and a BlobError with code too_large, so one error path renders both the client-side refusal and the server's.
The size check is the only one that runs in the browser. The server is authoritative for everything else. If the constraints have not arrived yet, the file is simply sent and the route decides.
Error codes#
A refusal here is too_large, content_type_not_allowed, invalid_content_type_pattern or empty_body. It reaches the browser as a BlobError with that code intact, so switch on error.code rather than on status numbers. See Errors.
Server-side writes#
bucket.put() takes the same contentTypes and maxSize options, with the same grammar, aliases and byte check, for bytes that pass through your own route. See Writing.