diff --git a/internal/rproxy/request.go b/internal/rproxy/request.go index 170b14e..bee4367 100644 --- a/internal/rproxy/request.go +++ b/internal/rproxy/request.go @@ -272,6 +272,10 @@ func (rq *request) modifyResponse(res *http.Response) error { rq.closeAfterAnswer() + // An answer that arrives before the app has been sent the whole request ends + // the request, and SWRP_MAX_RESPONSE_TIME_MS starts. + rq.startResponseTimer(requestEnd) + res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq} rq.out.afterStatus = rq.setResponseDeadline @@ -386,14 +390,17 @@ func (rq *request) finish(ctx context.Context) { // 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 + // reads the rest. The app's answer is cut off by a refusal once it is passed + // on, as afterStatus shows, since a refusal ends the request to the app. Any + // answer is cut off when the time to send it runs out: a flush then fails, 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): + case rq.out.afterStatus != nil && rq.refused.Load() != 0, + 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()) @@ -553,19 +560,29 @@ func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) { requestEnd := rq.endRequest() rq.mu.Lock() - defer rq.mu.Unlock() + stopTimer(rq.requestTimer) + rq.mu.Unlock() - if rq.timersStopped { + rq.startResponseTimer(requestEnd) +} + +// startResponseTimer starts SWRP_MAX_RESPONSE_TIME_MS, which runs from +// requestEnd, unless it has started already. +func (rq *request) startResponseTimer(requestEnd time.Time) { + limit := rq.h.config.MaxResponseTime + if limit == 0 { return } - stopTimer(rq.requestTimer) + rq.mu.Lock() + defer rq.mu.Unlock() - limit := rq.h.config.MaxResponseTime - if limit > 0 { - rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)), - rq.responseTimedOut) + if rq.timersStopped || rq.responseTimer != nil { + return } + + rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)), + rq.responseTimedOut) } // responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the