reverseproxy: log status 499 instead of 0 when client disconnects (#7827)
Some checks are pending
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Waiting to run
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Waiting to run
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Waiting to run
Tests / test (s390x on IBM Z) (push) Waiting to run
Tests / goreleaser-check (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, aix) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, linux) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, windows) (push) Waiting to run
Lint / lint (push) Waiting to run
Lint / lint-1 (push) Waiting to run
Lint / lint-2 (push) Waiting to run
Lint / govulncheck (push) Waiting to run
Lint / dependency-review (push) Waiting to run
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Waiting to run

This commit is contained in:
Saleh 2026-06-18 06:31:02 +03:00 committed by GitHub
parent ab56721a73
commit d2e0ad1e92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 3 deletions

View file

@ -0,0 +1,54 @@
package reverseproxy
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// TestClientDisconnectRecordsStatus verifies that when the downstream client
// disconnects (its request context is canceled) before the upstream sends any
// response headers, the recorded status is 499 ("client closed request")
// rather than 0.
func TestClientDisconnectRecordsStatus(t *testing.T) {
// backend that blocks until the client goes away, so it never gets
// the chance to send response headers
gotRequest := make(chan struct{})
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(gotRequest)
<-r.Context().Done()
}))
defer backend.Close()
h := minimalHandler(0, &Upstream{
Host: new(Host),
Dial: backend.Listener.Addr().String(),
})
ctx, cancel := context.WithCancel(context.Background())
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil).WithContext(ctx)
req = prepareTestRequest(req)
rec := caddyhttp.NewResponseRecorder(httptest.NewRecorder(), nil, nil)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
return nil
}))
}()
<-gotRequest
cancel()
wg.Wait()
if got := rec.Status(); got != 499 {
t.Errorf("expected status 499 after client disconnect, got %d", got)
}
}

View file

@ -699,9 +699,15 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// proxy the request to that upstream
proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
if proxyErr == nil || errors.Is(proxyErr, context.Canceled) {
// context.Canceled happens when the downstream client
// cancels the request, which is not our failure
if proxyErr == nil {
return true, nil
}
if errors.Is(proxyErr, context.Canceled) {
// context.Canceled happens when the downstream client cancels the
// request, which is not our failure; don't retry or ding the upstream.
// Record a 499 (client closed request) so the access log reflects the
// disconnect instead of a misleading 0 status (see #7396).
w.WriteHeader(499)
return true, nil
}