Uploading files

Any file that is not HTML or XML — images, CSS, JavaScript, JSON, PDFs, fonts, archives — is stored as an opaque blob and served back byte-for-byte. Blobs are not parsed, composed, or addressable by selector.

Uploading

PUT the file bytes to a path, with the file's media type as the Content-Type:

If /data.json has
PUT /data.json HTTP/2
Host: 127.0.0.1
Content-Type: application/json

{"name":"widget","price":29.99}

Fetching the path returns the stored bytes unchanged, with the stored content type:

GET /data.json HTTP/2
Host: 127.0.0.1
HTTP/2 200
content-type: application/json

{"name":"widget","price":29.99}

POST to a directory creates a new blob under a generated name, the same way it creates HTML resources — see Resource Creation.

Content type

The stored content type decides how a document is served and whether it is treated as HTML, XML, or an opaque blob. It is resolved in this order:

# Rule
1 A .html / .htm path is always text/html, even if the request sends a different Content-Type.
2 Otherwise, the explicit Content-Type request header, if present.
3 Otherwise, inferred from the file extension — .csstext/css, .pngimage/png, and so on.
4 Otherwise, application/octet-stream.

Because the explicit header wins over the extension (rule 2 before rule 3), send the correct Content-Type for the file — or omit the header entirely and let the extension decide. A wrong or generic header (for example, a client that defaults every body to application/x-www-form-urlencoded) is stored verbatim and served back as-is.

Serving

A blob GET returns the stored bytes with the stored Content-Type and a content-hash strong ETag, so conditional requests revalidate cheaply. Static-asset types — CSS, JavaScript, images, and fonts — are additionally served with Cache-Control: public, max-age, so caches and CDNs between tiers can store them.

Blobs are opaque:

Storage

Blob bytes are held in an external object store, not the primary document database. An operator configures one or more backends — filesystem, s3, or azure; when several are configured, reads try each in list order and writes and deletes fan out to all of them. This is an operator concern: authoring is the same PUT / GET regardless of the backend.

See also