Return and pass on request IDs, and give /v1/e/ ETag, 304 and HEAD (closes #84) #179
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user