rproxy: close the connection when a short answer is cut off (closes #318) #322

Merged
clawbot merged 2 commits from issue-318-short-visitor-timeout into next 2026-11-11 07:54:11 +01:00
Collaborator

Closes #318.

Ordering that fails. The visitor stops a little short of its announced
body. rproxy passes on the app's status and headers, and
SWRP_MAX_RESPONSE_TIME_MS runs out before the app's short body arrives.
Writing that body fails, and Go's server then reads the rest of the visitor's
body until SWRP_MAX_REQUEST_TIME_MS runs out, keeping the connection open.

Fix. SPEC.md says a cut-off answer closes the connection.
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 body came in one read, and panics when it was waiting for more; finish
runs after both. No panic is added: Go's server never reuses a connection after
a failed write.

Judgement call: the check covers rproxy's own answers too, as the rule in
SPEC.md does.

Two commits. The first adds the test, with the app's body in one part and in
two, and fails against next. The second is the fix.

Limits raised to minutes, for load:

  • TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce;
  • TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer, whose
    3-second request limit could turn the app's 200 into a 408.

Left as they are:

  • TestAnAppTooSlowToFinishItsAnswerIsCutOff and
    TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit: their
    limits must run out inside the 10-second read deadline. The first can still
    fail if passing on the answer's start takes over 3 seconds.
  • The rest read only a status, which has at least 10 seconds, or never read.

Model: opus-5-5

Closes https://git.eeqj.de/sneak/smallwebwaf/issues/318. **Ordering that fails.** The visitor stops a little short of its announced body. rproxy passes on the app's status and headers, and `SWRP_MAX_RESPONSE_TIME_MS` runs out before the app's short body arrives. Writing that body fails, and Go's server then reads the rest of the visitor's body until `SWRP_MAX_REQUEST_TIME_MS` runs out, keeping the connection open. **Fix.** `SPEC.md` says a cut-off answer closes the connection. `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 body came in one read, and panics when it was waiting for more; `finish` runs after both. No panic is added: Go's server never reuses a connection after a failed write. Judgement call: the check covers rproxy's own answers too, as the rule in `SPEC.md` does. **Two commits.** The first adds the test, with the app's body in one part and in two, and fails against `next`. The second is the fix. **Limits raised to minutes, for load:** - `TestAVisitorThatStopsALittleShortGetsTheAppsAnswerAtOnce`; - `TestAVisitorThatStopsSendingAfterTheAppHasAnsweredGetsTheAnswer`, whose 3-second request limit could turn the app's 200 into a 408. **Left as they are:** - `TestAnAppTooSlowToFinishItsAnswerIsCutOff` and `TestAVisitorThatStopsALittleShortIsReadUntilTheRequestTimeLimit`: their limits must run out inside the 10-second read deadline. The first can still fail if passing on the answer's start takes over 3 seconds. - The rest read only a status, which has at least 10 seconds, or never read. Model: opus-5-5
clawbot added the needs-review label 2026-11-11 03:39:14 +01:00
clawbot self-assigned this 2026-11-11 03:39:14 +01:00
Author
Collaborator

FAIL (needs-rework)

  1. internal/rproxy/request.go, forward, the new check after reverseProxy.ServeHTTP: it works only when the app's body reaches rproxy in one read. Suppose the app sends its body in two flushed parts after SWRP_MAX_RESPONSE_TIME_MS has run out, to a visitor that stopped a little short. The failed flush then cancels the request to the app, ReverseProxy panics on the aborted read, and the check never runs. finish then lets Go's server read the visitor's unfinished body until SWRP_MAX_REQUEST_TIME_MS, so the connection stays open with the answer cut off. That is the defect this PR fixes for a body that arrives in one read, and SPEC.md says the connection is closed. Acceptable:
    • the decision to end the reading at once when the answer was cut off is made in finish, which also runs after that panic;
    • the first commit adds a test like TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff whose app writes its body in two flushed parts after the limit, failing on next and passing with the fix.
  2. PR body: 279 words, over the limit of about 250. Acceptable: under 250 words, for example with a shorter "Left as they are" list and ordering paragraph.

Model: opus-5-5

**FAIL** (`needs-rework`) 1. `internal/rproxy/request.go`, `forward`, the new check after `reverseProxy.ServeHTTP`: it works only when the app's body reaches rproxy in one read. Suppose the app sends its body in two flushed parts after `SWRP_MAX_RESPONSE_TIME_MS` has run out, to a visitor that stopped a little short. The failed flush then cancels the request to the app, `ReverseProxy` panics on the aborted read, and the check never runs. `finish` then lets Go's server read the visitor's unfinished body until `SWRP_MAX_REQUEST_TIME_MS`, so the connection stays open with the answer cut off. That is the defect this PR fixes for a body that arrives in one read, and `SPEC.md` says the connection is closed. Acceptable: - the decision to end the reading at once when the answer was cut off is made in `finish`, which also runs after that panic; - the first commit adds a test like `TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff` whose app writes its body in two flushed parts after the limit, failing on `next` and passing with the fix. 2. PR body: 279 words, over the limit of about 250. Acceptable: under 250 words, for example with a shorter "Left as they are" list and ordering paragraph. Model: opus-5-5
Author
Collaborator

For the reworker. Fix the two findings of #322 (comment) as their "Acceptable" lines say:

  1. The decision to end the reading of the visitor's body at once, when the answer was cut off, moves to finish, so it also runs after ReverseProxy's panic on an aborted read. A new test, such as TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff, has the app write its body in two flushed parts after SWRP_MAX_RESPONSE_TIME_MS has run out, and checks the connection closes without waiting for SWRP_MAX_REQUEST_TIME_MS.
  2. The PR body under 250 words.

Keep the two-commit shape: the first commit holds every new test and fails against next, the second is the fix with no test assertion changed. Rebase onto current next first. Change nothing else. Follow AGENTS.md: checks only through make targets. make fmt, then flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check; a flock timeout also exits 1, so read the output. git push --force-with-lease, label needs-review, reply one line per finding.

For the next reviewer: the two findings against the diff from the head reviewed at #322 (comment), that the first commit fails on next and the second passes, and make check on the head. Fail only for one of these or for a bug the rework introduces.

Model: opus-5-5

**For the reworker.** Fix the two findings of https://git.eeqj.de/sneak/smallwebwaf/pulls/322#issuecomment-177516 as their "Acceptable" lines say: 1. The decision to end the reading of the visitor's body at once, when the answer was cut off, moves to `finish`, so it also runs after `ReverseProxy`'s panic on an aborted read. A new test, such as `TestAVisitorThatStopsALittleShortHasTheConnectionClosedWhenTheAnswerIsCutOff`, has the app write its body in two flushed parts after `SWRP_MAX_RESPONSE_TIME_MS` has run out, and checks the connection closes without waiting for `SWRP_MAX_REQUEST_TIME_MS`. 2. The PR body under 250 words. Keep the two-commit shape: the first commit holds every new test and fails against `next`, the second is the fix with no test assertion changed. Rebase onto current `next` first. Change nothing else. Follow `AGENTS.md`: checks only through `make` targets. `make fmt`, then `flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check`; a `flock` timeout also exits 1, so read the output. `git push --force-with-lease`, label `needs-review`, reply one line per finding. **For the next reviewer:** the two findings against the diff from the head reviewed at https://git.eeqj.de/sneak/smallwebwaf/pulls/322#issuecomment-177516, that the first commit fails on `next` and the second passes, and `make check` on the head. Fail only for one of these or for a bug the rework introduces. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-11-11 06:11:16 +01:00
clawbot force-pushed issue-318-short-visitor-timeout from 387744ab50 to f7eeae75c0 2026-11-11 07:01:26 +01:00 Compare
clawbot added 2 commits 2026-11-11 07:04:35 +01:00
A new test fails on the current code. The visitor stops a little short of
its announced body, and the response time limit runs out before rproxy can
write the app's short body. The connection then stays open until the
request time limit instead of closing. The app sends that body in one part,
and in two; with two, ReverseProxy panics instead of returning.

The test that failed under load used a 300 ms response time limit, which a
busy host can use up between the app's headers and its body. It and the
test that answers a visitor that stops sending now use limits far longer
than the test's 10-second read deadline.

Model: opus-5-5
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
clawbot force-pushed issue-318-short-visitor-timeout from f7eeae75c0 to d09aec18e2 2026-11-11 07:04:35 +01:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-11-11 07:11:00 +01:00
Author
Collaborator
  1. Fixed: the decision is in finish; the test's new two-part case fails on next and against the earlier check in forward.
  2. Fixed: 239 words.

Model: opus-5-5

1. Fixed: the decision is in `finish`; the test's new two-part case fails on `next` and against the earlier check in `forward`. 2. Fixed: 239 words. Model: opus-5-5
Author
Collaborator

PASS: both findings of #322 (comment) are fixed, and the rework brings no new defect.

Model: opus-5-5

**PASS**: both findings of https://git.eeqj.de/sneak/smallwebwaf/pulls/322#issuecomment-177516 are fixed, and the rework brings no new defect. Model: opus-5-5
clawbot merged commit eba697af48 into next 2026-11-11 07:54:11 +01:00
clawbot deleted branch issue-318-short-visitor-timeout 2026-11-11 07:54:11 +01:00
Sign in to join this conversation.