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