rproxy: cut off an answer that stalls after arriving before the whole request (closes #345)
check / check (push) Waiting to run
check / check (push) Waiting to run
When the app's answer arrived before the app had been sent the whole request, SWRP_MAX_RESPONSE_TIME_MS had no timer: only the end of the request started one. If the app then stalled, only a write could fail at the deadline. With nothing written, the connection stayed open until the request time limit. modifyResponse now starts the response timer when it ends the request. Once the timer cuts off the app's answer, nothing is left to flush, so the last flush in finish succeeded and Go's server read the rest of the body. finish now also ends that reading at once after a refusal of an answer already passed on. Model: opus-5-5
This commit was merged in pull request #348.
This commit is contained in:
1 parent
e3c5b4ecf8
commit
6a59011dee
1 file changed
+32
-14
+32
-14
@@ -272,6 +272,10 @@ func (rq *request) modifyResponse(res *http.Response) error {
|
|||||||
|
|
||||||
rq.closeAfterAnswer()
|
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}
|
res.Body = &responseBody{body: limitBody(res.Body, maxBytes), rq: rq}
|
||||||
rq.out.afterStatus = rq.setResponseDeadline
|
rq.out.afterStatus = rq.setResponseDeadline
|
||||||
|
|
||||||
@@ -386,14 +390,18 @@ func (rq *request) finish(ctx context.Context) {
|
|||||||
|
|
||||||
// After a cut-off answer, the reading of a body that has not arrived whole
|
// 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
|
// 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. modifyResponse sets afterStatus only when it passes the
|
||||||
// every flush after a failed one fails too. ReverseProxy ignores the failure,
|
// app's answer on. Any refusal after that has cut the answer off, since a
|
||||||
// but panics if it was still waiting for more of the app's answer; finish runs
|
// refusal ends the request to the app. Any answer is cut off when the time to
|
||||||
// either way. Before a status, a flush would send a 200; after a 101,
|
// send it runs out: a flush then fails, and every flush after a failed one
|
||||||
// ReverseProxy has taken over the connection.
|
// 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 {
|
switch {
|
||||||
case rq.out.status != 0 && rq.out.status != http.StatusSwitchingProtocols &&
|
case rq.out.afterStatus != nil && rq.refused.Load() != 0,
|
||||||
errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded):
|
rq.out.status != 0 && rq.out.status != http.StatusSwitchingProtocols &&
|
||||||
|
errors.Is(rq.rc.Flush(), os.ErrDeadlineExceeded):
|
||||||
rq.stopPassingOnBody(time.Now())
|
rq.stopPassingOnBody(time.Now())
|
||||||
case rq.refused.Load() == 0:
|
case rq.refused.Load() == 0:
|
||||||
rq.stopPassingOnBody(rq.requestDeadline())
|
rq.stopPassingOnBody(rq.requestDeadline())
|
||||||
@@ -553,19 +561,29 @@ func (rq *request) wroteRequest(info httptrace.WroteRequestInfo) {
|
|||||||
requestEnd := rq.endRequest()
|
requestEnd := rq.endRequest()
|
||||||
|
|
||||||
rq.mu.Lock()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTimer(rq.requestTimer)
|
rq.mu.Lock()
|
||||||
|
defer rq.mu.Unlock()
|
||||||
|
|
||||||
limit := rq.h.config.MaxResponseTime
|
if rq.timersStopped || rq.responseTimer != nil {
|
||||||
if limit > 0 {
|
return
|
||||||
rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)),
|
|
||||||
rq.responseTimedOut)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rq.responseTimer = time.AfterFunc(time.Until(requestEnd.Add(limit)),
|
||||||
|
rq.responseTimedOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
// responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the
|
// responseTimedOut is called when SWRP_MAX_RESPONSE_TIME_MS runs out before the
|
||||||
|
|||||||
Reference in new issue
Block a user