1 Commits
Author SHA1 Message Date
sneak 65560f7af8 Serve both image routes with one function and one error mapping (closes #316)
check / check (push) Canceled after 0s
HandleImage parses the path and verifies the signature, and HandleImageEnc
decrypts the token. Each then calls serveImage, which chooses the format for
auto, gets the image and serves it: the headers, ETag and 304, HEAD, the copy
and the log line. respondImageError answers the image service's errors on both
routes, and handleImageError is gone. /v1/image/ now answers 504 when the
upstream sends no response headers within upstream_fetch_timeout, as README.md
documented for /v1/e/. respondImageError logs a 500 at error level and every
other answer at warn. A failed write is logged once, with the request and the
bytes served. README.md lists each image route's answers as bullets. Tests send
the same errors through both routes.

Model: opus-5-5
2026-10-26 13:45:47 +00:00
5 changed files with 14 additions and 22 deletions
+1 -2
View File
@@ -289,8 +289,7 @@ path under `/v1/` answers 200, in maintenance mode too.
- 400 for a token that does not decrypt.
- 400 for a token that asks for a size or fit that is not valid.
- 410 once the token has expired.
- 400 when the token's format is `auto` and the `Accept` header is not
valid.
- 400 for an `Accept` header that is not valid.
- These answers, for the same causes as on `/v1/image/`:
- 406
- 403
+3 -6
View File
@@ -54,12 +54,9 @@ P2: security: per-origin rate limiting
warn level.
- The "image served" line carries the host, the path and `fetched_bytes` on
both routes. The debug line "encrypted image request" is gone.
- The debug line "failed to decrypt URL" is now "invalid encrypted URL". A
token that decrypts to a payload that does not decode also gets it.
- A failed write is logged once, as "failed to write response" at warn
level, since most often the client has gone away. It carries the
`request_id`, the host, the path, `served_bytes` and the error, and is not
also logged as "image served".
- A failed write is logged once, as "failed to write response" at error
level. It carries the `request_id`, the host, the path, `served_bytes` and
the error, and is not also logged as "image served".
- 2026-10-26 every key derived from the signing key is derived in one place
(closes #315): `seal.DeriveKeys` returns them as one `seal.Keys`, with their
salts declared beside it. `internal/providers` calls it once at startup. Each
+5 -8
View File
@@ -190,8 +190,8 @@ func (s *Handlers) validateSignature(
}
// parseExpires reads the exp query parameter, a Unix time in seconds. An exp
// missing from the URL gives the zero time, which the signature check refuses
// as a missing exp. An exp in the URL that is not a whole number, an empty
// missing from the URL gives the zero time, which the signature check takes
// as no expiration. An exp in the URL that is not a whole number, an empty
// one included, is an error naming exp and the value.
func parseExpires(query url.Values) (time.Time, error) {
if !query.Has("exp") {
@@ -221,9 +221,8 @@ func (s *Handlers) HandleTooManyRequests() http.HandlerFunc {
// respondImageError answers err, the image service's error for req, on both
// image routes. It logs a 500 at error level, as pixa's own failure. It logs
// the other answers at warn level, since the client, the upstream or a full
// limit caused them. The 415 for a source refused for its first bytes is not
// logged here, because the image service logs it.
// every other answer at warn level, since the client, the upstream or a full
// limit caused it.
func (s *Handlers) respondImageError(
w http.ResponseWriter, req *imagerequest.Request, err error,
) {
@@ -395,11 +394,9 @@ func (s *Handlers) serveImage(
// Stream the response
w.WriteHeader(http.StatusOK)
// A failed write is most often a client that has gone away, not pixa's
// failure, so it is logged at warn level
servedBytes, err := io.Copy(w, resp.Content)
if err != nil {
s.log.Warn("failed to write response",
s.log.Error("failed to write response",
"request_id", middleware.GetReqID(r.Context()),
"host", req.Source.Host,
"path", req.Source.Path,
@@ -518,7 +518,7 @@ func (failingResponseWriter) Write([]byte) (int, error) {
}
// TestImageRoutes_FailedWriteLoggedOnce checks that both image routes log a
// failed write of the image once, at warn level, with the request's ID, the
// failed write of the image once, at error level, with the request's ID, the
// source host and path, the bytes served and the error. Neither may also log
// the image as served.
func TestImageRoutes_FailedWriteLoggedOnce(t *testing.T) {
@@ -568,7 +568,7 @@ func TestImageRoutes_FailedWriteLoggedOnce(t *testing.T) {
}
want := map[string]any{
"level": slog.LevelWarn.String(),
"level": slog.LevelError.String(),
"request_id": requestID,
"host": signedHost,
"path": photoPath,
+3 -4
View File
@@ -54,7 +54,7 @@ func (s *Handlers) parseImageEncRequest(
return nil, time.Time{}, false
}
s.log.Debug("invalid encrypted URL", "error", err)
s.log.Debug("failed to decrypt URL", "error", err)
s.respondError(w, "invalid encrypted URL", http.StatusBadRequest)
return nil, time.Time{}, false
@@ -62,9 +62,8 @@ func (s *Handlers) parseImageEncRequest(
// Apply the same dimension and fit-mode bounds as the plain image
// route: a sealed payload is trusted for its origin, not for staying
// within limits. This refuses an over-limit size before the source is
// fetched. An unknown fit mode is a 400 here rather than a 500 from the
// processor.
// within limits, so an over-limit size or unknown fit mode is a 400
// here rather than an out-of-memory or a 500 from the processor.
err = imagerequest.ValidateImageRequest(req)
if err != nil {
s.log.Debug("encrypted URL failed validation", "error", err)