Return and pass on request IDs, and give /v1/e/ ETag, 304 and HEAD (closes #84)
check / check (push) Failing after 2s

Every response carries X-Request-Id, the upstream fetch sends it, and
the "upstream fetched", "image converted" and "image served" lines log
it as request_id. pixa's own RequestID middleware keeps a request's own
ID only when it is at most 64 letters, digits, '-', '_' or '.', and
otherwise makes a random one with crypto/rand, so nothing a client
chooses freely and nothing about the host reaches upstream. /v1/e/ now
sets ETag, answers a matching If-None-Match with 304 and is routed for
HEAD, through notModified, which both image handlers call. No Vary is
added: go-chi/cors already sends Vary: Origin.

Model: opus-5-5
This commit was merged in pull request #179.
This commit is contained in:
2026-10-04 12:41:54 +02:00
parent 363774c058
commit c7173c47d8
15 changed files with 619 additions and 58 deletions
+15 -3
View File
@@ -120,8 +120,9 @@ path under `/v1/` answers 200, in maintenance mode too.
`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.
- `GET /v1/e/<token>/<name>` — an image through an encrypted URL (see Encrypted
URLs). Needs: nothing but the URL. Answers: 200; 400 for a token that does not
- `GET` or `HEAD` `/v1/e/<token>/<name>` — an image through an encrypted URL
(see Encrypted URLs). Needs: nothing but the URL. Answers: 200; 304 when
`If-None-Match` matches the image's `ETag`; 400 for a token that does not
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
@@ -137,6 +138,15 @@ path under `/v1/` answers 200, in maintenance mode too.
authentication with `metrics.username` and `metrics.password`. Answers: 200;
401 without them; 404 when they are not set, as the route then does not exist.
Every response carries an `X-Request-ID` header holding the request's ID, which
a client can quote when reporting a problem: the request's own `X-Request-ID`,
as a reverse proxy in front of pixa may send, when it is at most 64 letters,
digits, `-`, `_` or `.`; otherwise a random one pixa makes for the request,
which tells nothing about the machine or the other requests. pixa's log line for
the request carries the same ID as `request_id`, and so do the lines it logs
when it fetches, converts and serves an image; the fetch sends it to the
upstream host as `X-Request-ID`.
Both `POST` routes accept only a form that pixa's own page served: the page puts
a token in the form and sets a cookie to match, and a request without both is
refused with 403, so another site cannot submit the form from a visitor's
@@ -186,7 +196,9 @@ source) and one transcode: the first request does the work, and the others wait
for its image or its error, holding no upstream connection or processing slot
of their own. A waiting request stops waiting when its own client goes away.
The work goes on for the others even if the first request's client goes away,
until that request's `downstream_timeout` ends.
until that request's `downstream_timeout` ends. The shared fetch sends the first
request's ID upstream, and the lines logged for the fetch and the transcode
carry that ID.
The login form (`POST /`) is limited to 5 attempts per minute per client
address, counting an IPv6 client by its /64; an attempt over the limit is