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
@@ -0,0 +1,118 @@
package middleware
import (
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/go-chi/chi/v5/middleware"
"sneak.berlin/go/pixa/internal/config"
)
// sendRequestID sends a request through the RequestID middleware, carrying
// incoming as its X-Request-Id unless that is empty. It returns the ID the
// next handler found in the request context, which the upstream fetch sends
// and the log lines carry, and the X-Request-Id of the response.
func sendRequestID(t *testing.T, incoming string) (string, string) {
t.Helper()
mw := &Middleware{log: slog.Default(), config: &config.Config{}}
var inContext string
handler := mw.RequestID()(http.HandlerFunc(
func(_ http.ResponseWriter, r *http.Request) {
inContext = middleware.GetReqID(r.Context())
}))
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
if incoming != "" {
req.Header.Set("X-Request-Id", incoming)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
return inContext, rec.Header().Get("X-Request-Id")
}
// TestRequestIDKeepsShortPlainID verifies that a request's own X-Request-Id of
// at most 64 letters, digits, '-', '_' or '.' is kept as its ID.
func TestRequestIDKeepsShortPlainID(t *testing.T) {
t.Parallel()
for _, incoming := range []string{
"client-request-id",
"A1_b2.c3-d4",
strings.Repeat("a", 64),
} {
inContext, inResponse := sendRequestID(t, incoming)
if inContext != incoming || inResponse != incoming {
t.Errorf("incoming %q: context has %q, response %q, want both %q",
incoming, inContext, inResponse, incoming)
}
}
}
// TestRequestIDReplacesLongOrUnusualID verifies that a request's own
// X-Request-Id that is over 64 characters or holds anything but letters,
// digits, '-', '_' or '.' is neither sent back nor sent upstream: the request
// gets a fresh ID instead.
func TestRequestIDReplacesLongOrUnusualID(t *testing.T) {
t.Parallel()
for _, incoming := range []string{
strings.Repeat("a", 65),
strings.Repeat("a", 9000),
"has space",
"a/b",
"a,b",
"<script>",
"ünicode",
} {
inContext, inResponse := sendRequestID(t, incoming)
t.Logf("incoming %.20q: made up %q", incoming, inResponse)
if inResponse == "" || inResponse == incoming {
t.Errorf("incoming %.20q: response has %q, want a fresh ID",
incoming, inResponse)
}
if inContext != inResponse {
t.Errorf("incoming %.20q: context has %q, want the response's %q",
incoming, inContext, inResponse)
}
}
}
// TestRequestIDMadeUpTellsNothing verifies that the ID made up for a request
// that sent none differs for every request and does not hold the host name.
func TestRequestIDMadeUpTellsNothing(t *testing.T) {
t.Parallel()
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("os.Hostname() error = %v", err)
}
firstInContext, first := sendRequestID(t, "")
_, second := sendRequestID(t, "")
t.Logf("host %q, made up %q and %q", hostname, first, second)
if first == "" || first == second {
t.Errorf("made up %q and %q, want two different IDs", first, second)
}
if firstInContext != first {
t.Errorf("context has %q, want the response's %q", firstInContext, first)
}
if strings.Contains(first, hostname) {
t.Errorf("made-up ID %q holds the host name %q", first, hostname)
}
}