diff --git a/SPEC.md b/SPEC.md index 5cdaea5..b3677f6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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 diff --git a/docs/settings.md b/docs/settings.md index 18b7dee..df6c14f 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -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 diff --git a/internal/rproxy/limits_test.go b/internal/rproxy/limits_test.go index 57224e7..544dc14 100644 --- a/internal/rproxy/limits_test.go +++ b/internal/rproxy/limits_test.go @@ -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() diff --git a/internal/rproxy/request.go b/internal/rproxy/request.go index 9c7b224..b7401a4 100644 --- a/internal/rproxy/request.go +++ b/internal/rproxy/request.go @@ -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))) } }