Answer 499 to an image request whose client went away (closes #339)
check / check (push) Canceled after 0s

When the client of an image request goes away before its image is
ready, the image proxy returns context.Canceled. serveImage now logs
this once at info level, with the request_id, host and path, and
answers 499, the status nginx calls "client closed request". It was
answered 500 and logged at error level, as pixa's own failure. The
check sits in serveImage, before respondImageError, which cannot see
the request's context. A cancellation while the request's context is
live is still answered 500. README.md lists the 499 on both image
routes.

Model: opus-5-5
This commit is contained in:
2026-10-26 22:31:56 +00:00
parent 6de69f7349
commit dc71ed125b
4 changed files with 209 additions and 2 deletions
+24 -2
View File
@@ -1,6 +1,7 @@
package handlers
import (
"context"
"errors"
"fmt"
"io"
@@ -19,6 +20,11 @@ import (
"sneak.berlin/go/pixa/internal/magic"
)
// statusClientClosedRequest is the status nginx calls "client closed
// request". The image routes answer it to a request whose client went away
// before its image was ready. 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 +351,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 went away before the image was ready, and 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 +369,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)