@marcel91200 / caddy Mirror · read-only / commits / d2e0ad1e
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 08:30 UTC

reverseproxy: log status 499 instead of 0 when client disconnects (#7827)

Saleh committed Jun 18, 2026 at 06:31 UTC d2e0ad1e92329d841a3a12e9ec7935afb30ce246
2 files changed +63 -3
modules/caddyhttp/reverseproxy/client_disconnect_test.go new
+54
@@ -0,0 +1,54 @@
1 +package reverseproxy
2 +
3 +import (
4 + "context"
5 + "net/http"
6 + "net/http/httptest"
7 + "sync"
8 + "testing"
9 +
10 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
11 +)
12 +
13 +// TestClientDisconnectRecordsStatus verifies that when the downstream client
14 +// disconnects (its request context is canceled) before the upstream sends any
15 +// response headers, the recorded status is 499 ("client closed request")
16 +// rather than 0.
17 +func TestClientDisconnectRecordsStatus(t *testing.T) {
18 + // backend that blocks until the client goes away, so it never gets
19 + // the chance to send response headers
20 + gotRequest := make(chan struct{})
21 + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 + close(gotRequest)
23 + <-r.Context().Done()
24 + }))
25 + defer backend.Close()
26 +
27 + h := minimalHandler(0, &Upstream{
28 + Host: new(Host),
29 + Dial: backend.Listener.Addr().String(),
30 + })
31 +
32 + ctx, cancel := context.WithCancel(context.Background())
33 + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
34 + req = prepareTestRequest(req)
35 +
36 + rec := caddyhttp.NewResponseRecorder(httptest.NewRecorder(), nil, nil)
37 +
38 + var wg sync.WaitGroup
39 + wg.Add(1)
40 + go func() {
41 + defer wg.Done()
42 + _ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
43 + return nil
44 + }))
45 + }()
46 +
47 + <-gotRequest
48 + cancel()
49 + wg.Wait()
50 +
51 + if got := rec.Status(); got != 499 {
52 + t.Errorf("expected status 499 after client disconnect, got %d", got)
53 + }
54 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+9 -3
@@ -699,9 +699,15 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
699
700 // proxy the request to that upstream
701 proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
702 - if proxyErr == nil || errors.Is(proxyErr, context.Canceled) {
703 - // context.Canceled happens when the downstream client
704 - // cancels the request, which is not our failure
702 + if proxyErr == nil {
703 + return true, nil
704 + }
705 + if errors.Is(proxyErr, context.Canceled) {
706 + // context.Canceled happens when the downstream client cancels the
707 + // request, which is not our failure; don't retry or ding the upstream.
708 + // Record a 499 (client closed request) so the access log reflects the
709 + // disconnect instead of a misleading 0 status (see #7396).
710 + w.WriteHeader(499)
711 return true, nil
712 }
713