rproxy: send its own answers at once to a visitor that stops sending its body (closes #303)
check / check (push) Failing after 18m11s

An answer rproxy gives itself, such as a deny or a refusal, waited while
Go's server read up to 256 KiB of a body that had not arrived whole. A
visitor that stopped sending got nothing until SWRP_MAX_REQUEST_TIME_MS
ran out, and no bytes at all once that was past the 10 seconds the
answer has to be sent.

send now sets Connection: close in that case, as modifyResponse does for
the app's answer; both call closeAfterAnswer. requestTimedOut no longer
ends that read of Go's server, which no longer happens, so the time the
app's answer started is no longer noted.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 22:28:39 +00:00
1 parent f64752e3b7
commit 4c5aef63bc
3 files changed
+26 -37

No files matched your search

+7 -6
View File
@@ -974,7 +974,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
}
}
func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing.T) {
func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
@@ -993,14 +993,15 @@ func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing
standIn := startDiscriminator(t, deny)
app := startApp(t, greet)
// The time limit is long enough for a loaded host to read the first part of
// the body before it runs out.
// The request time limit is longer than the 10 seconds an answer rproxy
// gives itself has to be sent, and than dial's deadline.
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
maxRequestTime: "3000",
maxRequestTime: "15000",
maxResponseTime: "300",
})
// Go's server waits for a little of the body left before it sends an answer,
// and the time for sending the request ends that wait.
// The body stops 100 bytes short, less than Go's server reads of a body
// left unread before it answers.
conn := dial(t, addr)
send(t, conn, tc.start+announcedLength(questionBodyMaxBytes+100)+
strings.Repeat("x", questionBodyMaxBytes))
+15 -31
View File
@@ -83,8 +83,8 @@ type request struct {
upgraded *upgradedConn
// mu guards what follows. The time limits run on goroutines of their own,
// and the transport starts and stops them, and notes the times below, from
// its own; once timersStopped is set, neither time limit acts any more.
// and the transport starts and stops them, and notes requestEnd, from its
// own; once timersStopped is set, neither time limit acts any more.
mu sync.Mutex
timersStopped bool
requestTimer *time.Timer
@@ -92,8 +92,6 @@ type request struct {
// requestEnd is when the app had been sent the whole request, or the answer
// started, if that was sooner.
requestEnd time.Time
// answerStarted is when the first byte of the app's answer arrived.
answerStarted time.Time
}
// newRequest starts handling r: it notes the time, works out the visitor from
@@ -177,8 +175,7 @@ func (rq *request) forward(ctx context.Context) {
rq.cancel = cancel
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: rq.wroteRequest,
GotFirstResponseByte: rq.gotFirstResponseByte,
WroteRequest: rq.wroteRequest,
})
out := rq.in.WithContext(ctx)
@@ -273,12 +270,7 @@ func (rq *request) modifyResponse(res *http.Response) error {
rq.out.Header()["Content-Type"] = nil
}
// Go's server reads up to 256 KiB of a body left unread before it sends an
// answer, and so waits on a visitor that has stopped sending. With this
// header it sends the answer at once, and closes the connection after it.
if rq.body != nil && !rq.body.received.Load() {
rq.out.Header().Set("Connection", "close")
}
rq.closeAfterAnswer()
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
rq.out.afterStatus = rq.setResponseDeadline
@@ -347,11 +339,22 @@ func (rq *request) send(status int, body []byte) {
rq.out.Header().Set("Content-Length", strconv.Itoa(len(body)))
}
rq.closeAfterAnswer()
rq.out.WriteHeader(status)
_, _ = rq.out.Write(body)
_ = rq.rc.Flush()
}
// closeAfterAnswer has Go's server send the answer at once, and close the
// connection after it, when the visitor's body has not arrived whole. Without
// it, Go's server reads up to 256 KiB of the body left before it sends an
// answer, and so waits on a visitor that has stopped sending.
func (rq *request) closeAfterAnswer() {
if rq.body != nil && !rq.body.received.Load() {
rq.out.Header().Set("Connection", "close")
}
}
// prepareOwnAnswer readies the connection for an answer rproxy gives itself: the
// reading of a body that has not arrived whole ends at deadline, and the answer
// has SWRP_MAX_RESPONSE_TIME_MS from now to reach the visitor, even when the app
@@ -517,16 +520,6 @@ func (rq *request) requestTimedOut() {
}
if rq.body == nil || !rq.body.waiting.Load() {
// Once the app has answered, Go's server can read what is left of a body
// under 256 KiB without requestBody.Read, before an answer rproxy gives
// itself, so waiting is false while that read waits for the visitor.
// That read ends now, before refuse:
// should rproxy still answer itself, answer then sets the deadline it
// gives its own answers.
if !rq.answerStarted.IsZero() {
rq.stopReadingBody(time.Now())
}
rq.refuse(http.StatusGatewayTimeout)
return
@@ -539,15 +532,6 @@ func (rq *request) requestTimedOut() {
_ = rq.rc.SetReadDeadline(time.Now())
}
// gotFirstResponseByte is called once the first byte of the app's answer has
// arrived.
func (rq *request) gotFirstResponseByte() {
rq.mu.Lock()
defer rq.mu.Unlock()
rq.answerStarted = time.Now()
}
// wroteRequest is called once the app has been sent the whole request:
// SWRP_MAX_REQUEST_TIME_MS ends and SWRP_MAX_RESPONSE_TIME_MS starts.
func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {