rproxy: close the connection when a short answer is cut off (closes #318)
check / check (push) Failing after 3m22s
check / check (push) Failing after 3m22s
When SWRP_MAX_RESPONSE_TIME_MS ran out before the last of a short answer was written, the flush failed and ReverseProxy ignored it. If the visitor had stopped short of its announced body, Go's server then read the rest until SWRP_MAX_REQUEST_TIME_MS ran out, keeping the connection open with the answer cut off. Go does that read even after an aborting panic. forward now checks for a failed flush once ReverseProxy returns. It then records a 504 refusal, ends the reading of the body at once, and panics so that Go's server closes the connection, as SPEC.md says a cut-off answer does. Model: opus-5-5
This commit is contained in:
1 parent
b36a2a6df2
commit
6cd91fb30b
1 file changed
+20
-6
@@ -195,6 +195,20 @@ func (rq *request) forward(ctx context.Context) {
|
|||||||
rq.startRequestTimer()
|
rq.startRequestTimer()
|
||||||
rq.upstreamStart = time.Now()
|
rq.upstreamStart = time.Now()
|
||||||
reverseProxy.ServeHTTP(rq.out, out)
|
reverseProxy.ServeHTTP(rq.out, out)
|
||||||
|
|
||||||
|
// ReverseProxy ignores a failed flush, so it misses a short answer cut off when
|
||||||
|
// SWRP_MAX_RESPONSE_TIME_MS runs out; once a flush has failed, every later one
|
||||||
|
// fails too. Go's server would then keep the connection open while it reads
|
||||||
|
// the rest of a body that has not arrived whole. So that reading ends now, and
|
||||||
|
// the refusal keeps finish from extending it to SWRP_MAX_REQUEST_TIME_MS. The
|
||||||
|
// panic has Go's server close the connection, and log nothing; the deferred
|
||||||
|
// finish still runs.
|
||||||
|
if rq.out.status != http.StatusSwitchingProtocols &&
|
||||||
|
errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded) {
|
||||||
|
rq.refuse(http.StatusGatewayTimeout)
|
||||||
|
rq.stopPassingOnBody(time.Now())
|
||||||
|
panic(http.ErrAbortHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewrite makes the request the app receives: the visitor's request, unchanged,
|
// rewrite makes the request the app receives: the visitor's request, unchanged,
|
||||||
@@ -385,7 +399,7 @@ func (rq *request) finish(ctx context.Context) {
|
|||||||
rq.stopTimers()
|
rq.stopTimers()
|
||||||
|
|
||||||
if rq.refused.Load() == 0 {
|
if rq.refused.Load() == 0 {
|
||||||
rq.stopPassingOnBody()
|
rq.stopPassingOnBody(rq.requestDeadline())
|
||||||
}
|
}
|
||||||
|
|
||||||
requestEnd := rq.endRequest()
|
requestEnd := rq.endRequest()
|
||||||
@@ -473,10 +487,10 @@ func (rq *request) stopReadingBody(deadline time.Time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stopPassingOnBody ends the transport's reading of a visitor's body that has
|
// 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
|
// not arrived whole, and lets Go's server read the rest until deadline. Go's
|
||||||
// SWRP_MAX_REQUEST_TIME_MS runs out. Go's server reads it once the handler
|
// server reads it once the handler returns, but with no deadline at all if
|
||||||
// returns, but with no deadline at all if another read of it is still waiting.
|
// another read of it is still waiting.
|
||||||
func (rq *request) stopPassingOnBody() {
|
func (rq *request) stopPassingOnBody(deadline time.Time) {
|
||||||
if rq.body == nil || rq.body.received.Load() {
|
if rq.body == nil || rq.body.received.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -489,7 +503,7 @@ func (rq *request) stopPassingOnBody() {
|
|||||||
rq.body.stopped = true
|
rq.body.stopped = true
|
||||||
rq.body.mu.Unlock()
|
rq.body.mu.Unlock()
|
||||||
|
|
||||||
rq.stopReadingBody(rq.requestDeadline())
|
rq.stopReadingBody(deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRequestTimer starts SWRP_MAX_REQUEST_TIME_MS, which runs from the
|
// startRequestTimer starts SWRP_MAX_REQUEST_TIME_MS, which runs from the
|
||||||
|
|||||||
Reference in new issue
Block a user