rproxy: leave no visitor without an answer as the response time runs out (closes #295)
check / check (push) Failing after 5m35s

A visitor could get a closed connection and no answer when
SWRP_MAX_RESPONSE_TIME_MS ran out about when the answer started: the
write deadline passed before the first byte was written.

- An answer rproxy gives itself has at least 10 seconds to be sent.
- An app's answer that arrives once the time has run out is answered
  504.
- Otherwise the app's status and headers are sent at once, before the
  body is read, with at least 10 seconds. The exact limit applies to
  the rest.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 18:21:01 +00:00
1 parent 8ab4280792
commit fb13d37fa0
5 files changed
+136 -12

No files matched your search

+38 -9
View File
@@ -32,6 +32,17 @@ const reportStatusMax = 599
// plainText is the Content-Type of the answers rproxy gives itself.
const plainText = "text/plain; charset=utf-8"
// minSendTime is the least time to send an answer rproxy gives itself, or the
// status and headers of the app's answer. A shorter time can run out before the
// first byte is written, on a busy host or as SWRP_MAX_RESPONSE_TIME_MS runs out,
// and the visitor then gets a closed connection and no answer.
const minSendTime = 10 * time.Second
// errResponseTooLate refuses an app's answer that arrives once
// SWRP_MAX_RESPONSE_TIME_MS has run out.
var errResponseTooLate = errors.New(
"the answer arrived after SWRP_MAX_RESPONSE_TIME_MS ran out")
// request is one request on its way through rproxy, from the moment its headers
// have been read to its log line and report.
type request struct {
@@ -237,6 +248,17 @@ func (rq *request) modifyResponse(res *http.Response) error {
return nil
}
requestEnd := rq.endRequest()
// An answer that arrives once SWRP_MAX_RESPONSE_TIME_MS has run out is not
// passed on: the visitor is answered 504.
limit := rq.h.config.MaxResponseTime
if limit > 0 && !time.Now().Before(requestEnd.Add(limit)) {
rq.refuse(http.StatusGatewayTimeout)
return errResponseTooLate
}
maxBytes := rq.h.config.MaxResponseSize
if maxBytes > 0 && res.Body != http.NoBody && res.ContentLength > maxBytes {
rq.refuse(http.StatusBadGateway)
@@ -252,7 +274,7 @@ func (rq *request) modifyResponse(res *http.Response) error {
}
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
rq.setResponseDeadline()
rq.out.afterStatus = rq.setResponseDeadline
return nil
}
@@ -326,14 +348,14 @@ func (rq *request) send(status int, body []byte) {
// 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
// used that time up first.
// used that time up first, and at least minSendTime.
func (rq *request) prepareOwnAnswer(deadline time.Time) {
rq.stopReadingBody(deadline)
rq.endRequest()
limit := rq.h.config.MaxResponseTime
if limit > 0 {
_ = rq.rc.SetWriteDeadline(time.Now().Add(limit))
_ = rq.rc.SetWriteDeadline(time.Now().Add(max(limit, minSendTime)))
}
}
@@ -550,15 +572,22 @@ func (rq *request) responseReceived() {
}
// setResponseDeadline sets SWRP_MAX_RESPONSE_TIME_MS on the connection to the
// visitor: the app's answer must reach the visitor within it of the end of the
// request.
// visitor once ReverseProxy has written the status of the app's answer: the
// answer must reach the visitor within it of the end of the request. First it
// sends the status and headers, with minSendTime at least, before ReverseProxy
// reads the body: the time running out can still cut the answer off, but the
// visitor never gets a closed connection with no answer at all.
func (rq *request) setResponseDeadline() {
requestEnd := rq.endRequest()
limit := rq.h.config.MaxResponseTime
if limit > 0 {
_ = rq.rc.SetWriteDeadline(requestEnd.Add(limit))
if limit == 0 {
return
}
deadline := rq.endRequest().Add(limit)
_ = rq.rc.SetWriteDeadline(time.Now().Add(max(time.Until(deadline), minSendTime)))
_ = rq.rc.Flush()
_ = rq.rc.SetWriteDeadline(deadline)
}
// endRequest notes now as the end of the request, unless an end is noted, and