Return and pass on request IDs, and give /v1/e/ ETag, 304 and HEAD (closes #84) #179

Merged
clawbot merged 8 commits from issue-84-response-headers into next 2026-10-04 12:41:55 +02:00
5 changed files with 35 additions and 19 deletions
Showing only changes of commit c1c28204a1 - Show all commits
+7 -6
View File
@@ -139,12 +139,13 @@ path under `/v1/` answers 200, in maintenance mode too.
401 without them; 404 when they are not set, as the route then does not exist. 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 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` a client can quote when reporting a problem: the request's own `X-Request-ID`,
when it sent one, as a reverse proxy in front of pixa may, otherwise one pixa as a reverse proxy in front of pixa may send, when it is at most 64 letters,
makes up from its host name, a random string chosen at startup and a counter. digits, `-`, `_` or `.`; otherwise a random one pixa makes for the request,
pixa's log line for the request carries the same ID as `request_id`, and so do which tells nothing about the machine or the other requests. pixa's log line for
the lines it logs when it fetches, converts and serves an image; the fetch sends the request carries the same ID as `request_id`, and so do the lines it logs
it to the upstream host as `X-Request-ID`. 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 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 a token in the form and sets a cookie to match, and a request without both is
+4 -3
View File
@@ -30,9 +30,10 @@ P2: security: referer blacklist
# Completed Steps # Completed Steps
- 2026-10-04 request IDs returned and passed on, and `/v1/e/` revalidates - 2026-10-04 request IDs returned and passed on, and `/v1/e/` revalidates
(closes #84): a middleware right after chi's `RequestID` sets `X-Request-ID` (closes #84): pixa's own `RequestID` middleware, in place of chi's, gives each
on every response from the ID `RequestID` stores in the request context, which request an ID, its own `X-Request-ID` when that is at most 64 letters, digits,
is the request's own `X-Request-ID` when it sent one; the upstream fetch sends `-`, `_` or `.` and a random one otherwise, stores it where chi's did and
sends it back as `X-Request-ID` on every response; the upstream fetch sends
that ID, and the "upstream fetched", "image converted" and "image served" log that ID, and the "upstream fetched", "image converted" and "image served" log
lines carry it as `request_id`, a fetch shared by several requests carrying lines carry it as `request_id`, a fetch shared by several requests carrying
the first request's; `/v1/e/` sets `ETag`, answers a matching `If-None-Match` the first request's; `/v1/e/` sets `ETag`, answers a matching `If-None-Match`
@@ -11,7 +11,7 @@ import (
) )
// TestFetchSendsRequestID verifies that a fetch sends the ID of the request // TestFetchSendsRequestID verifies that a fetch sends the ID of the request
// it serves, which chi's RequestID middleware stores in the request context, // it serves, which the RequestID middleware stores in the request context,
// to the upstream host as X-Request-Id, so the fetch can be found in that // to the upstream host as X-Request-Id, so the fetch can be found in that
// host's logs. // host's logs.
func TestFetchSendsRequestID(t *testing.T) { func TestFetchSendsRequestID(t *testing.T) {
+22 -7
View File
@@ -2,9 +2,12 @@
package middleware package middleware
import ( import (
"context"
"crypto/rand"
"log/slog" "log/slog"
"net/http" "net/http"
"net/netip" "net/netip"
"regexp"
"time" "time"
basicauth "github.com/99designs/basicauth-go" basicauth "github.com/99designs/basicauth-go"
@@ -115,16 +118,28 @@ func (s *Middleware) RateLimit(
}) })
} }
// RequestIDResponseHeader returns a middleware that sends the request's ID as // requestIDPattern is what a request's own X-Request-Id must look like to be
// kept as its ID: 1 to 64 letters, digits, '-', '_' or '.'.
var requestIDPattern = regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`)
// RequestID returns a middleware that gives each request an ID and sends it as
// the X-Request-Id response header, so a client can quote it when reporting a // the X-Request-Id response header, so a client can quote it when reporting a
// problem. The ID is the one chi's RequestID middleware stored in the request // problem. The ID is the request's own X-Request-Id when that matches
// context, so RequestID must run first. // requestIDPattern, and otherwise a random one, which tells nothing about the
func (s *Middleware) RequestIDResponseHeader() func(http.Handler) http.Handler { // machine or the traffic. It is stored in the request context under chi's
// RequestIDKey, where the logging middleware, the handlers and the upstream
// fetch read it.
func (s *Middleware) RequestID() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(middleware.RequestIDHeader, id := r.Header.Get(middleware.RequestIDHeader)
middleware.GetReqID(r.Context())) if !requestIDPattern.MatchString(id) {
next.ServeHTTP(w, r) id = rand.Text()
}
w.Header().Set(middleware.RequestIDHeader, id)
ctx := context.WithValue(r.Context(), middleware.RequestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
} }
+1 -2
View File
@@ -28,8 +28,7 @@ func (s *Server) SetupRoutes() {
s.router = chi.NewRouter() s.router = chi.NewRouter()
s.router.Use(middleware.Recoverer) s.router.Use(middleware.Recoverer)
s.router.Use(middleware.RequestID) s.router.Use(s.mw.RequestID())
s.router.Use(s.mw.RequestIDResponseHeader())
s.router.Use(s.mw.ClientIP()) s.router.Use(s.mw.ClientIP())
s.router.Use(s.mw.SecurityHeaders()) s.router.Use(s.mw.SecurityHeaders())
s.router.Use(s.mw.Logging()) s.router.Use(s.mw.Logging())