Answer 499 to a client that goes away while waiting for an image (closes #339)
check / check (push) Canceled after 0s

When a request waits for an image another request is making and its
client goes away, the image proxy returns context.Canceled. serveImage
now answers it 499, the status nginx calls "client closed request". It
logs it once at info level with the request_id, host and path. It was
answered 500 and logged at error level, as pixa's own failure. A
cancellation while the request's context is live is still answered 500.
The request making the image is still answered as before. README.md
lists the 499 on both image routes.

Model: opus-5-5
This commit is contained in:
2026-10-26 23:14:10 +00:00
parent fc4c6b1a90
commit 0f788e05b7
3 changed files with 39 additions and 2 deletions
+3
View File
@@ -310,6 +310,8 @@ path under `/v1/` answers 200, in maintenance mode too.
- 403 when the upstream host, or a host it redirects to, has an address in a
blocked network (see `blocked_networks`).
- 429 over `image_requests_per_minute` (below).
- 499 when the client goes away while waiting for an image another request
is making (below).
- 415 when the upstream answers with a `Content-Type` that is not one of the
source formats (below).
- 415 when the upstream sends a body whose first bytes are not those of the
@@ -341,6 +343,7 @@ path under `/v1/` answers 200, in maintenance mode too.
- 403
- 415
- 429
- 499
- 502
- 503
- 504
+11
View File
@@ -30,6 +30,17 @@ P2: security: per-origin rate limiting
# Completed Steps
- 2026-10-26 on both image routes, a request is answered 499 and logged at info
level when its client goes away while waiting for an image another request is
making (closes #339). It was answered 500 and logged at error level, as pixa's
own failure.
- The line is "client went away before the image was ready". It carries the
`request_id`, the host and the path.
- The 499 has no body, since nobody reads it.
- `serveImage` answers it before calling `respondImageError`, which cannot
see the request's context.
- An error wrapping `context.Canceled` while the request's context is live
is still answered 500 and logged at error level.
- 2026-10-26 `README.md`'s Design section starts with a Concepts list (closes
#321). Each concept gets one sentence, under the name the code gives it.
- Storage calls the transformed images variants, in shorter sentences.
+25 -2
View File
@@ -1,6 +1,7 @@
package handlers
import (
"context"
"errors"
"fmt"
"io"
@@ -19,6 +20,12 @@ import (
"sneak.berlin/go/pixa/internal/magic"
)
// statusClientClosedRequest is the status nginx calls "client closed
// request". The image routes answer it when the client goes away while
// waiting for an image another request is making. Nobody reads it; it is for
// logs and metrics.
const statusClientClosedRequest = 499
// HandleImage handles the main image proxy route:
// /v1/image/<host>/<path>/<width>x<height>.<format>, or with no format
// /v1/image/<host>/<path>/<width>x<height>
@@ -345,8 +352,9 @@ func notModified(w http.ResponseWriter, r *http.Request, etag string) bool {
// serveImage serves the image that req asks for. Both image routes call it
// once they have the request and its URL's expiry, which ends the
// Cache-Control max-age. It chooses the format for the format auto, answers
// If-None-Match with 304 and HEAD with the headers only, and answers an error
// of the image service with respondImageError.
// If-None-Match with 304 and HEAD with the headers only. It answers 499 when
// the client goes away while waiting for an image another request is making.
// It answers any other error of the image service with respondImageError.
func (s *Handlers) serveImage(
w http.ResponseWriter, r *http.Request,
req *imagerequest.Request, expires time.Time,
@@ -362,6 +370,21 @@ func (s *Handlers) serveImage(
startTime := time.Now()
resp, err := s.imgSvc.Get(r.Context(), req)
// The client went away before its image was ready, which is not pixa's
// failure. A cancellation while the request's context is live is pixa's,
// and respondImageError answers it 500.
if errors.Is(err, context.Canceled) && r.Context().Err() != nil {
s.log.Info("client went away before the image was ready",
"request_id", middleware.GetReqID(r.Context()),
"host", req.Source.Host,
"path", req.Source.Path,
)
w.WriteHeader(statusClientClosedRequest)
return
}
if err != nil {
s.respondImageError(w, req, err)