rproxy: give its own answers at least 10 seconds to be sent (closes #295)
check / check (push) Successful in 3m52s

The write deadline of an answer rproxy gives itself was
SWRP_MAX_RESPONSE_TIME_MS from when the answer started. With a limit
under a second, a host too busy to run rproxy for that long passes the
deadline before the first byte is written. Go then closes the
connection and the visitor gets no answer.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 17:11:13 +00:00
1 parent 8ab4280792
commit 27835a703f
4 files changed
+35 -4

No files matched your search

+2 -1
View File
@@ -2245,7 +2245,8 @@ These limits apply per request, in both directions:
- `SWRP_MAX_RESPONSE_TIME_MS` runs from the end of the request to the last byte
of the answer reaching the visitor. The request ends when the app has been
sent all of it, or when the app's answer arrives if that is sooner. An answer
`rproxy` gives itself has the whole time from when it starts.
`rproxy` gives itself has the whole time from when it starts, and never less
than 10 seconds.
The largest request line and headers is 32 KiB. A kept-open connection is closed
after 120 seconds idle. Neither has a setting. traefik, by default, closes an
+2 -1
View File
@@ -436,7 +436,8 @@ A time here is a whole number of milliseconds above zero, such as `2000`.
headers, the time until the app has been sent the whole request. A time or
`off`.
- `SWRP_MAX_RESPONSE_TIME_MS` (default `1800000`): how long the answer may take,
from the end of the request to its last byte reaching the visitor. A time or
from the end of the request to its last byte reaching the visitor. An answer
`rproxy` gives itself always has at least 10 seconds to be sent. A time or
`off`.
- `SWRP_LISTEN_ADDR` (default `:8080`): where `rproxy` listens, such as `:9000`.
- `SWRP_UPSTREAM_URL` (default `http://127.0.0.1:8081`): the app. An `http` or
+24
View File
@@ -481,6 +481,30 @@ func TestAnAnswerRProxyGivesItselfHasTheResponseTimeToBeSent(t *testing.T) {
}
}
func TestAnAnswerRProxyGivesItselfHasTenSecondsToBeSentAtLeast(t *testing.T) {
t.Parallel()
const least = 10 * time.Second
standIn := startDiscriminator(t, deny)
app := startApp(t, greet)
_, _, server := startServer(t, standIn.URL, app.URL, map[string]string{
maxResponseTime: "300",
})
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
w := &deadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
before := time.Now()
server.Handler.ServeHTTP(w, req)
if w.writeDeadline.Before(before.Add(least)) {
t.Errorf("the time limit on sending ends at %s, want %s from the answer at least",
w.writeDeadline, least)
}
}
func TestAVisitorTooSlowToSendTheRestIsAnswered408(t *testing.T) {
t.Parallel()
+7 -2
View File
@@ -32,6 +32,11 @@ const reportStatusMax = 599
// plainText is the Content-Type of the answers rproxy gives itself.
const plainText = "text/plain; charset=utf-8"
// ownAnswerMinSendTime is the least time an answer rproxy gives itself has to be
// sent. A shorter time can run out on a busy host before the first byte is
// written, and the visitor then gets a closed connection and no answer.
const ownAnswerMinSendTime = 10 * time.Second
// 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 {
@@ -326,14 +331,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 ownAnswerMinSendTime.
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, ownAnswerMinSendTime)))
}
}