Serve the format auto, chosen from the Accept header (closes #88)
check / check (push) Failing after 2s

auto is a format in the /v1/image/ path, an encrypted URL's token and
the generator page. Once the signature or token is checked, pixa
chooses AVIF when Accept names image/avif, else WebP when it names
image/webp, else JPEG when the most specific of image/jpeg, image/* and
*/* allows it or Accept is absent. q=0 refuses a format; a header
allowing none of the three answers 406, one that does not parse 400.
The signature and token cover auto itself; the cache key and ETag use
the chosen format. Answers from then on carry Vary: Accept.

Model: opus-5-5
This commit is contained in:
2026-10-05 02:42:58 +00:00
parent 39a647b6c5
commit 9a5ef06c80
9 changed files with 189 additions and 14 deletions
+32 -10
View File
@@ -175,12 +175,14 @@ path under `/v1/` answers 200, in maintenance mode too.
- `GET` or `HEAD` `/v1/image/<host>/<path>/<size>.<format>` — an image, fetched,
resized and converted (below). Needs: a signature, unless the host is
allowlisted (see Source Hosts). Answers: 200; 304 when `If-None-Match` matches
the image's `ETag`; 400 for a URL or parameter that is not valid; 401 for a
missing or wrong signature, a missing `exp` or an `exp` in the past; 403 when
the request's `Referer` names a host in `referer_blocklist`, checked before
the signature, the cache and the upstream fetch; 403 when the upstream host,
or a host it redirects to, is `localhost`, ends in `.localhost` or `.local`,
or has an address in a blocked network (see `blocked_networks`); 502 when the
the image's `ETag`; 400 for a URL or parameter that is not valid, or for the
format `auto` an `Accept` header that is not valid; 406 for the format `auto`
when `Accept` allows none of the formats it chooses from; 401 for a missing or
wrong signature, a missing `exp` or an `exp` in the past; 403 when the
request's `Referer` names a host in `referer_blocklist`, checked before the
signature, the cache and the upstream fetch; 403 when the upstream host, or a
host it redirects to, is `localhost`, ends in `.localhost` or `.local`, or has
an address in a blocked network (see `blocked_networks`); 502 when the
upstream answered with an error status, and for 5 minutes after that for the
same source URL; 503 when pixa is busy or in maintenance mode; 500 for any
other failure.
@@ -190,7 +192,8 @@ path under `/v1/` answers 200, in maintenance mode too.
decrypt, or that asks for a size or fit that is not valid; 410 once it has
expired; 504 when the upstream has not sent its response headers within
`upstream_fetch_timeout`, but 500 when that time runs out while the image
itself is still arriving; 403, 502, 503 and 500 as for `/v1/image/`.
itself is still arriving; 400 for an `Accept` header that is not valid, and
406, 403, 502, 503 and 500, as for `/v1/image/`.
- `GET /robots.txt` — asks every crawler to stay away (`Disallow: /`). Needs:
nothing. Answers: 200.
- `GET /.well-known/healthcheck.json` — JSON with `status` (`ok`), `now`,
@@ -239,7 +242,7 @@ A request whose query string cannot be decoded, or gives any parameter more than
once, is refused with 400.
- `<format>`: one of `orig` (or `original`), `jpeg` (or `jpg`), `png`, `webp`,
`avif`, `gif`
`avif`, `gif`, or `auto` (below)
- `<size>`: `orig` or `<width>x<height>` (e.g. `800x600`)
- `sig` and `exp`: the signature and its expiry, needed unless the host is
allowlisted (see Signature Specification)
@@ -247,6 +250,24 @@ once, is refused with 400.
both optional (values under Signature Specification). Both are part of what is
cached, so each value of either is a separate cached image.
With the format `auto`, pixa chooses the format for each request from its
`Accept` header, in this order:
1. AVIF, when the header names `image/avif`;
2. WebP, when it names `image/webp`;
3. JPEG, when the first of `image/jpeg`, `image/*` and `*/*` that it names
allows it, or when there is no `Accept` header or it is empty.
An entry with `q=0` refuses its format; other `q` values do not change the
order. AVIF and WebP must be named, as clients that cannot show them also send
`image/*` and `*/*`. pixa never sends a format the client refused: when the
header allows none of the three, the answer is 406, and a header that does not
parse, or has a `q` that is not a number from 0 to 1, is refused with 400. The
signature, or the token of an encrypted URL, covers `auto` itself, so one URL
serves every client. Each format chosen is cached as a separate image, and every
answer that depends on `Accept` (the image, a 304, and the 400 and 406 above)
carries `Vary: Accept`, so a shared cache keeps the formats apart too.
An image is served with `Cache-Control: public, max-age=<seconds>, immutable`.
When the URL has an expiry (an `exp`, or the TTL of an encrypted URL), `max-age`
is the whole seconds left until then, at most one year, so no browser or proxy
@@ -297,7 +318,7 @@ nor change what it asks for.
3. The page shows the URL, `https://<host>/v1/e/<token>/img.<format>`, and when
it expires. `<host>` is the host the page was opened on, and the URL starts
with `http` instead while `debug` is on. The name after the token is ignored
and only gives the URL a file extension, `jpg` for `orig`.
and only gives the URL a file extension, `jpg` for `orig` and `auto`.
The token holds the source's host, path and query and the size, format, quality,
fit and expiry, encrypted with a key derived from `signing_key`. The source
@@ -353,7 +374,8 @@ Where:
- `width` — requested width in pixels, `0` for original
- `height` — requested height in pixels, `0` for original
- `format` — output format, one of those listed under Routes, with `original`
signed as `orig` and `jpg` as `jpeg`
signed as `orig` and `jpg` as `jpeg`; `auto` is signed as `auto`, not as the
format chosen for the request
- `expiration` — the URL's `exp` query parameter, the Unix timestamp when the
signature expires; a request whose `exp` is not a whole number, an empty
`exp=` included, is refused with 400