diff --git a/internal/proxy/request.go b/internal/proxy/request.go index 1b60d07..8881934 100644 --- a/internal/proxy/request.go +++ b/internal/proxy/request.go @@ -789,9 +789,19 @@ func (rq *request) responseTimedOut() { } } -// responseReceived is called once the app has sent its whole answer. +// responseReceived is called once the app has sent its whole answer. An +// app may answer before the client has sent its whole body, as with a 404 +// it sends without reading it. The timeouts then run on: until the +// transport's read of the body returns, Go's server cannot send the +// answer, and SWWAF_CLIENT_REQUEST_TIMEOUT is what ends that read for a +// client that stops sending. func (rq *request) responseReceived() { rq.complete = true + + if rq.body != nil && !rq.body.received.Load() { + return + } + rq.stopTimers() } diff --git a/internal/proxy/timeouts_test.go b/internal/proxy/timeouts_test.go index dab7791..1616d4f 100644 --- a/internal/proxy/timeouts_test.go +++ b/internal/proxy/timeouts_test.go @@ -194,6 +194,28 @@ func sendLargeBody(t *testing.T, addr string) net.Conn { return conn } +func TestClientStopsSendingAfterTheAppHasAnswered(t *testing.T) { + t.Parallel() + + // The app answers at once without reading the body, as an app does + // for a path it does not serve. + app := startApp(t, http.NotFound) + addr, out := startProxy(t, app.URL, map[string]string{ + clientRequestTimeout: shortTimeoutSetting, + metricsToken: token, + }) + + start := time.Now() + got := readResponse(t, sendPartOfBody(t, addr)) + wantTimedOut(t, start) + + // The app's answer came before the time ran out, so the client is + // sent that answer, not smallwebwaf's 408. + wantStatus(t, got, http.StatusNotFound) + wantLine(t, out.requestLine(t), http.StatusNotFound, requestlog.ActionTimedOut) + wantLimitHits(t, addr, clientRequestTimeout, 1) +} + func TestAppTooSlowToAnswer(t *testing.T) { t.Parallel()