Author SHA1 Message Date
sneak 3c75f29f3c rproxy: send its own answers at once to a visitor that stops sending its body (closes #303)
check / check (push) Failing after 5m23s
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
2026-11-10 21:51:02 +00:00
3 changed files with 23 additions and 34 deletions

No files matched your search

+4
View File
@@ -2355,6 +2355,10 @@ These limits apply per request, in both directions:
than 10 seconds. The status and headers of the app's answer also have at least than 10 seconds. The status and headers of the app's answer also have at least
10 seconds to be sent. 10 seconds to be sent.
`rproxy` may give its own answer, such as a refusal, before the visitor has sent
its whole body. The visitor gets that answer at once, and the connection closes
after it.
The largest request line and headers is 32 KiB. A kept-open connection is closed The largest request line and headers is 32 KiB. A kept-open connection is closed
after 120 seconds idle. Neither has a setting. traefik, by default, closes an after 120 seconds idle. Neither has a setting. traefik, by default, closes an
unused connection after 90 seconds. So traefik closes first, and never sends a unused connection after 90 seconds. So traefik closes first, and never sends a
+7 -6
View File
@@ -974,7 +974,7 @@ func TestTheRequestTimeLimitDoesNotCutAnAnswerOnceTheAppHasTheRequest(t *testing
} }
} }
func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing.T) { func TestAVisitorThatStopsALittleShortGetsAnAnswerRProxyGivesAtOnce(t *testing.T) {
t.Parallel() t.Parallel()
for _, tc := range []struct { for _, tc := range []struct {
@@ -993,14 +993,15 @@ func TestAVisitorThatStopsSendingGetsAnAnswerRProxyGivesByTheDeadline(t *testing
standIn := startDiscriminator(t, deny) standIn := startDiscriminator(t, deny)
app := startApp(t, greet) app := startApp(t, greet)
// The time limit is long enough for a loaded host to read the first part of // The request time limit is longer than the 10 seconds an answer rproxy
// the body before it runs out. // gives itself has to be sent, and than dial's deadline.
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{ 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, // The body stops 100 bytes short, less than Go's server reads of a body
// and the time for sending the request ends that wait. // left unread before it answers.
conn := dial(t, addr) conn := dial(t, addr)
send(t, conn, tc.start+announcedLength(questionBodyMaxBytes+100)+ send(t, conn, tc.start+announcedLength(questionBodyMaxBytes+100)+
strings.Repeat("x", questionBodyMaxBytes)) strings.Repeat("x", questionBodyMaxBytes))
+12 -28
View File
@@ -92,8 +92,6 @@ type request struct {
// requestEnd is when the app had been sent the whole request, or the answer // requestEnd is when the app had been sent the whole request, or the answer
// started, if that was sooner. // started, if that was sooner.
requestEnd time.Time 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 // newRequest starts handling r: it notes the time, works out the visitor from
@@ -178,7 +176,6 @@ func (rq *request) forward(ctx context.Context) {
rq.cancel = cancel rq.cancel = cancel
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: rq.wroteRequest, WroteRequest: rq.wroteRequest,
GotFirstResponseByte: rq.gotFirstResponseByte,
}) })
out := rq.in.WithContext(ctx) out := rq.in.WithContext(ctx)
@@ -273,12 +270,7 @@ func (rq *request) modifyResponse(res *http.Response) error {
rq.out.Header()["Content-Type"] = nil rq.out.Header()["Content-Type"] = nil
} }
// Go's server reads up to 256 KiB of a body left unread before it sends an rq.closeAfterAnswer()
// 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")
}
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq} res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
rq.out.afterStatus = rq.setResponseDeadline 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.out.Header().Set("Content-Length", strconv.Itoa(len(body)))
} }
rq.closeAfterAnswer()
rq.out.WriteHeader(status) rq.out.WriteHeader(status)
_, _ = rq.out.Write(body) _, _ = rq.out.Write(body)
_ = rq.rc.Flush() _ = 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 // 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 // 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 // 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() { 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) rq.refuse(http.StatusGatewayTimeout)
return return
@@ -539,15 +532,6 @@ func (rq *request) requestTimedOut() {
_ = rq.rc.SetReadDeadline(time.Now()) _ = 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: // 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. // SWRP_MAX_REQUEST_TIME_MS ends and SWRP_MAX_RESPONSE_TIME_MS starts.
func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) { func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {