diff --git a/internal/proxy/request.go b/internal/proxy/request.go index 5eedc12..1155ea2 100644 --- a/internal/proxy/request.go +++ b/internal/proxy/request.go @@ -715,6 +715,12 @@ func (rq *request) requestTimedOut(limit string) { action: requestlog.ActionTimedOut, limit: limit, }) + // Go's server, before it sends the app's answer, and the + // transport, as it closes the body, read what is left of a body + // under 256 KiB without requestBody.Read, so waiting is false + // while they wait for the client. Those reads end when the client + // is out of time. + rq.stopReadingBody(rq.clientRequestDeadline()) return } diff --git a/internal/proxy/timeouts_test.go b/internal/proxy/timeouts_test.go index ff00c8c..8018d1b 100644 --- a/internal/proxy/timeouts_test.go +++ b/internal/proxy/timeouts_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strconv" + "strings" "sync" "sync/atomic" "testing" @@ -248,6 +249,59 @@ func TestClientStopsSendingAfterTheAppHasAnswered(t *testing.T) { wantLimitHits(t, addr, clientRequestTimeout, 1) } +func TestClientStopsSendingWithLittleOfItsBodyLeft(t *testing.T) { + t.Parallel() + + // appClosed is closed once smallwebwaf has closed its connection to + // the app, which it does once it has the app's answer. + appClosed := make(chan struct{}) + + // The app answers as soon as it has the headers, without reading the + // body, and says it closes the connection. + app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { + conn, buf, err := http.NewResponseController(w).Hijack() + if err != nil { + return + } + + _, _ = buf.WriteString("HTTP/1.1 404 Not Found\r\n" + + "Content-Length: 0\r\nConnection: close\r\n\r\n") + _ = buf.Flush() + _, _ = io.Copy(io.Discard, buf.Reader) + _ = conn.Close() + + close(appClosed) + }) + addr, out := startProxy(t, app.URL, map[string]string{ + clientRequestTimeout: shortTimeoutSetting, + metricsToken: token, + }) + + // Less than 256 KiB of the body is left after the first bytes, little + // enough for Go's server in smallwebwaf to read the rest itself before + // it sends the app's answer. + start := time.Now() + conn := dial(t, addr) + send(t, conn, "POST /upload HTTP/1.1\r\nHost: app\r\nContent-Length: 200000\r\n\r\n"+ + "the first bytes") + + select { + case <-appClosed: + case <-time.After(waitLimit): + t.Fatal("smallwebwaf did not close its connection to the app") + } + + // More of the body, which can no longer go to the app, and then + // nothing. + send(t, conn, strings.Repeat("x", 8<<10)) + + got := readResponse(t, conn) + wantTimedOut(t, start) + 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()