From f7eeae75c03727309d1591c8fbae5abefb0e5232 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Nov 2026 06:01:16 +0000 Subject: [PATCH] rproxy: close the connection when a short answer is cut off (closes #318) When SWRP_MAX_RESPONSE_TIME_MS ran out before the last of a short answer was written, the flush failed. 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. finish now flushes once more, and if that fails at its deadline, it ends the reading of the body at once. ReverseProxy ignores the failed flush when the app's answer came in one read, and panics when it was still waiting for more; finish runs after both. Go's server never reuses a connection after a failed write, so it closes it. Model: opus-5-5 --- internal/rproxy/request.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/internal/rproxy/request.go b/internal/rproxy/request.go index c346dae..170b14e 100644 --- a/internal/rproxy/request.go +++ b/internal/rproxy/request.go @@ -384,8 +384,19 @@ func (rq *request) refuse(status int) { func (rq *request) finish(ctx context.Context) { rq.stopTimers() - if rq.refused.Load() == 0 { - rq.stopPassingOnBody() + // After a cut-off answer, the reading of a body that has not arrived whole + // ends now. Go's server would otherwise keep the connection open while it + // reads the rest. A flush fails if the time to send the answer ran out, and + // every flush after a failed one fails too. ReverseProxy ignores the failure, + // but panics if it was still waiting for more of the app's answer; finish runs + // either way. Before a status, a flush would send a 200; after a 101, + // ReverseProxy has taken over the connection. + switch { + case rq.out.status != 0 && rq.out.status != http.StatusSwitchingProtocols && + errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded): + rq.stopPassingOnBody(time.Now()) + case rq.refused.Load() == 0: + rq.stopPassingOnBody(rq.requestDeadline()) } requestEnd := rq.endRequest() @@ -473,10 +484,10 @@ func (rq *request) stopReadingBody(deadline time.Time) { } // 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() { +// not arrived whole, and lets Go's server read the rest until deadline. 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(deadline time.Time) { if rq.body == nil || rq.body.received.Load() { return } @@ -489,7 +500,7 @@ func (rq *request) stopPassingOnBody() { rq.body.stopped = true rq.body.mu.Unlock() - rq.stopReadingBody(rq.requestDeadline()) + rq.stopReadingBody(deadline) } // startRequestTimer starts SWRP_MAX_REQUEST_TIME_MS, which runs from the