From 3a94293bee3944f6ee9d5d2b9dad9fe2ec64a2d1 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Nov 2026 06:01:16 +0000 Subject: [PATCH] rproxy tests: a cut-off answer must close the connection, and the stop-short tests no longer race the clock A new test fails on the current code. The visitor stops a little short of its announced body, and the response time limit runs out before rproxy can write the app's short body. The connection then stays open until the request time limit instead of closing. The app sends that body in one part, and in two; with two, ReverseProxy panics instead of returning. The test that failed under load used a 300 ms response time limit, which a busy host can use up between the app's headers and its body. It and the test that answers a visitor that stops sending now use limits far longer than the test's 10-second read deadline. Model: opus-5-5 --- internal/rproxy/limits_test.go | 89 ++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/internal/rproxy/limits_test.go b/internal/rproxy/limits_test.go index 685f05b..2af3b1f 100644 --- a/internal/rproxy/limits_test.go +++ b/internal/rproxy/limits_test.go @@ -12,6 +12,7 @@ import ( "os" "strconv" "strings" + "sync" "sync/atomic" "testing" "time" @@ -807,8 +808,10 @@ func TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer(t *testing. app := startApp(t, func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, greeting) }) + // The request time limit is far longer than dial's deadline, so that a busy + // host cannot use it up before the app's answer arrives. addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ - maxRequestTime: "3000", + maxRequestTime: "120000", }) // Far more announced than sent, so that Go's server answers for the app @@ -869,11 +872,11 @@ func TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce(t *testing.T) { standIn := startWAF(t, allow) app := startApp(t, answerBeforeTheBody) - // The request time limit is longer than the 10 seconds the status and headers - // of an answer have to be sent, and than dial's deadline. + // Both time limits are far longer than dial's deadline, so that the answer + // has to come before either runs out, and a busy host cannot use them up. addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ - maxRequestTime: "15000", - maxResponseTime: "300", + maxRequestTime: "120000", + maxResponseTime: "60000", }) conn := dial(t, addr) @@ -910,6 +913,82 @@ func TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit(t *testing. } } +func TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff( + t *testing.T, +) { + t.Parallel() + + const limit = 300 * time.Millisecond + + // With two parts, rproxy fails to send the first and then waits for the + // second, which the app sends only once rproxy has ended the request to it. + for _, tc := range []struct { + name string + parts []string + }{ + {"with the body in one part", []string{greeting}}, + {"with the body in two parts", []string{greeting[:5], greeting[5:]}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + headersRead := make(chan struct{}) + // letAppSendBody also runs when the test ends, as the app's server waits + // for the app. + letAppSendBody := sync.OnceFunc(func() { close(headersRead) }) + standIn := startWAF(t, allow) + // The app sends its body once the visitor has the status and headers and + // the response time limit has run out since, so that rproxy cannot send + // it. After each part, it reads the request's body, which ends when rproxy + // ends the request to it. + app := startApp(t, func(w http.ResponseWriter, r *http.Request) { + rc := http.NewResponseController(w) + _ = rc.EnableFullDuplex() + + w.Header().Set(contentLengthHeader, strconv.Itoa(len(greeting))) + + _ = rc.Flush() + + <-headersRead + time.Sleep(limit) + + for _, part := range tc.parts { + _, _ = io.WriteString(w, part) + _ = rc.Flush() + _, _ = io.Copy(io.Discard, r.Body) + } + }) + + t.Cleanup(letAppSendBody) + + // The request time limit is far longer than dial's deadline. + addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ + maxRequestTime: "120000", + maxResponseTime: strconv.FormatInt(limit.Milliseconds(), 10), + }) + + conn := dial(t, addr) + sendAllButALittle(t, conn) + + res, err := http.ReadResponse(bufio.NewReader(conn), nil) + if err != nil { + t.Fatalf("read the answer: %v", err) + } + + letAppSendBody() + + // If the connection stays open, dial's deadline ends this read with a + // timeout. + body, err := io.ReadAll(res.Body) + _ = res.Body.Close() + + if len(body) != 0 || !errors.Is(err, io.ErrUnexpectedEOF) { + t.Errorf("read %q (%v), want the connection closed", body, err) + } + }) + } +} + func TestAVisitorThatStopsSendingWhileTheAppWorksGetsNoAnswer(t *testing.T) { t.Parallel()