1 Commits
Author SHA1 Message Date
sneak dc71ed125b 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
2026-10-26 22:31:56 +00:00
4 changed files with 10 additions and 13 deletions

No files matched your search

+1 -2
View File
@@ -310,8 +310,7 @@ 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 - 403 when the upstream host, or a host it redirects to, has an address in a
blocked network (see `blocked_networks`). blocked network (see `blocked_networks`).
- 429 over `image_requests_per_minute` (below). - 429 over `image_requests_per_minute` (below).
- 499 when the client goes away while waiting for an image another request - 499 when the client goes away before the image is ready.
is making (below).
- 415 when the upstream answers with a `Content-Type` that is not one of the - 415 when the upstream answers with a `Content-Type` that is not one of the
source formats (below). source formats (below).
- 415 when the upstream sends a body whose first bytes are not those of the - 415 when the upstream sends a body whose first bytes are not those of the
+3 -4
View File
@@ -30,10 +30,9 @@ P2: security: per-origin rate limiting
# Completed Steps # Completed Steps
- 2026-10-26 on both image routes, a request is answered 499 and logged at info - 2026-10-26 on both image routes, a request whose client went away before its
level when its client goes away while waiting for an image another request is image was ready is answered 499 and logged at info level (closes #339). It was
making (closes #339). It was answered 500 and logged at error level, as pixa's answered 500 and logged at error level, as pixa's own failure.
own failure.
- The line is "client went away before the image was ready". It carries the - The line is "client went away before the image was ready". It carries the
`request_id`, the host and the path. `request_id`, the host and the path.
- The 499 has no body, since nobody reads it. - The 499 has no body, since nobody reads it.
@@ -109,8 +109,8 @@ func TestImageRoutes_ClientGoneAnswers499(t *testing.T) {
close(fetcher.release) close(fetcher.release)
<-firstDone <-firstDone
if rec.Code != 499 { if rec.Code != statusClientClosedRequest {
t.Errorf("status = %d, want 499", rec.Code) t.Errorf("status = %d, want %d", rec.Code, statusClientClosedRequest)
} }
var clientGoneLines int var clientGoneLines int
+4 -5
View File
@@ -21,9 +21,8 @@ import (
) )
// statusClientClosedRequest is the status nginx calls "client closed // statusClientClosedRequest is the status nginx calls "client closed
// request". The image routes answer it when the client goes away while // request". The image routes answer it to a request whose client went away
// waiting for an image another request is making. Nobody reads it; it is for // before its image was ready. Nobody reads it; it is for logs and metrics.
// logs and metrics.
const statusClientClosedRequest = 499 const statusClientClosedRequest = 499
// HandleImage handles the main image proxy route: // HandleImage handles the main image proxy route:
@@ -353,8 +352,8 @@ func notModified(w http.ResponseWriter, r *http.Request, etag string) bool {
// once they have the request and its URL's expiry, which ends the // 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 // Cache-Control max-age. It chooses the format for the format auto, answers
// If-None-Match with 304 and HEAD with the headers only. It answers 499 when // 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. // the client went away before the image was ready, and any other error of
// It answers any other error of the image service with respondImageError. // the image service with respondImageError.
func (s *Handlers) serveImage( func (s *Handlers) serveImage(
w http.ResponseWriter, r *http.Request, w http.ResponseWriter, r *http.Request,
req *imagerequest.Request, expires time.Time, req *imagerequest.Request, expires time.Time,