rproxy: answer a visitor that stops sending its body after the app has answered (closes #298)
check / check (push) Failing after 10m1s

When the app answered before the visitor's body had arrived whole, Go's
server read up to 256 KiB of the rest before it sent the answer. 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
status has to be sent.

Such an answer now carries Connection: close, which Go sends at once.
finish first ends the transport's read of the body: once the handler
returns, Go's server reads the rest with no deadline while another read
of it waits. requestBody.Close does nothing, so that ReverseProxy's
close does not wait on the visitor.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 19:45:28 +00:00
1 parent aeaa9d1c77
commit be74eb69f8
4 files changed
+156 -20

No files matched your search

+33 -15
View File
@@ -273,6 +273,13 @@ 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")
}
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
rq.out.afterStatus = rq.setResponseDeadline
@@ -375,7 +382,7 @@ func (rq *request) finish(ctx context.Context) {
rq.stopTimers()
if rq.refused.Load() == 0 {
rq.stopReadingBody(rq.requestDeadline())
rq.stopPassingOnBody()
}
requestEnd := rq.endRequest()
@@ -462,6 +469,26 @@ func (rq *request) stopReadingBody(deadline time.Time) {
_ = rq.rc.SetReadDeadline(deadline)
}
// stopPassingOnBody ends the transport's reading of a visitor's body that has
// not arrived whole, and lets Go's server read the rest until
// SWRP_MAX_REQUEST_TIME_MS runs out. Go's server reads it once the handler
// returns, but with no deadline at all if another read of it is still waiting.
func (rq *request) stopPassingOnBody() {
if rq.body == nil || rq.body.received.Load() {
return
}
// A deadline that has passed ends a Read that waits, and the lock waits for
// that Read to return.
_ = rq.rc.SetReadDeadline(time.Now())
rq.body.mu.Lock()
rq.body.stopped = true
rq.body.mu.Unlock()
rq.stopReadingBody(rq.requestDeadline())
}
// startRequestTimer starts SWRP_MAX_REQUEST_TIME_MS, which runs from the
// request's headers being read until the app has been sent the whole request.
func (rq *request) startRequestTimer() {
@@ -490,10 +517,10 @@ func (rq *request) requestTimedOut() {
}
if rq.body == nil || !rq.body.waiting.Load() {
// Once the app has answered, the transport, ReverseProxy and Go's server
// each close the visitor's body, and closing it reads what is left of a
// body under 256 KiB without requestBody.Read, so waiting is false while
// that read waits for the visitor. That read ends now, before refuse:
// 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() {
@@ -557,17 +584,8 @@ func (rq *request) responseTimedOut() {
}
}
// responseReceived is called once the app has sent its whole answer. An app may
// answer before the visitor has sent its whole body, as with a 404 it sends
// without reading it. The time limits then run on: until the transport's read of
// the body returns, Go's server cannot send the answer, and
// SWRP_MAX_REQUEST_TIME_MS is what ends that read for a visitor that stops
// sending.
// responseReceived is called once the app has sent its whole answer.
func (rq *request) responseReceived() {
if rq.body != nil && !rq.body.received.Load() {
return
}
rq.stopTimers()
}