rproxy: a visitor that stalls its own body after the app answers gets no bytes #298

Closed
opened 2026-11-10 19:27:07 +01:00 by clawbot · 2 comments
Collaborator

Found while fixing #295 (#297 (comment)), and on next too: when the app answers before the visitor has sent its whole body, and the visitor then stalls past both the 10-second send minimum and SWRP_MAX_RESPONSE_TIME_MS, the connection closes with no bytes. Go's server sends nothing until the visitor's body ends. Seen with SWRP_MAX_RESPONSE_TIME_MS=300 and SWRP_MAX_REQUEST_TIME_MS=15000.

Start after that PR lands.

Definition of done, one PR into next, in internal/rproxy:

  • First establish whether a visitor that stalls its own body can be given an answer at all once the app has answered: say on this issue what Go's server allows, and what the visitor sees today.
  • If an answer is possible, the visitor gets the app's answer or a 504, never a closed connection with no bytes; a test fails without the change.
  • If it is not possible without a large change, post the options and a recommendation here, assigned to the manager, before writing code; SPEC.md then says what such a visitor gets.
  • make fmt, then flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check passes. Label the PR needs-review.

Model: opus-5-5

Found while fixing https://git.eeqj.de/sneak/smallwebwaf/issues/295 (https://git.eeqj.de/sneak/smallwebwaf/pulls/297#issuecomment-174645), and on `next` too: when the app answers before the visitor has sent its whole body, and the visitor then stalls past both the 10-second send minimum and `SWRP_MAX_RESPONSE_TIME_MS`, the connection closes with no bytes. Go's server sends nothing until the visitor's body ends. Seen with `SWRP_MAX_RESPONSE_TIME_MS=300` and `SWRP_MAX_REQUEST_TIME_MS=15000`. Start after that PR lands. **Definition of done**, one PR into `next`, in `internal/rproxy`: - First establish whether a visitor that stalls its own body can be given an answer at all once the app has answered: say on this issue what Go's server allows, and what the visitor sees today. - If an answer is possible, the visitor gets the app's answer or a 504, never a closed connection with no bytes; a test fails without the change. - If it is not possible without a large change, post the options and a recommendation here, assigned to the manager, before writing code; `SPEC.md` then says what such a visitor gets. - `make fmt`, then `flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check` passes. Label the PR `needs-review`. Model: opus-5-5
clawbot self-assigned this 2026-11-10 19:27:07 +01:00
Author
Collaborator

Plan. #297 has landed. Work in internal/rproxy and its tests. Answer the first point of the definition of done on this issue, with what you observed, before writing code. Repeated runs of one test while finding out are the one exception to AGENTS.md's rule of checks only through make targets, and are not committed. make fmt, then flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check; a flock timeout also exits 1, so read the output. Start no subagent on another model. Title ending (closes #298).

Model: opus-5-5

**Plan.** https://git.eeqj.de/sneak/smallwebwaf/pulls/297 has landed. Work in `internal/rproxy` and its tests. Answer the first point of the definition of done on this issue, with what you observed, before writing code. Repeated runs of one test while finding out are the one exception to `AGENTS.md`'s rule of checks only through `make` targets, and are not committed. `make fmt`, then `flock -w 1800 /srv/code/tmp/.gate-smallwebwaf.lock make check`; a `flock` timeout also exits 1, so read the output. Start no subagent on another model. Title ending ` (closes #298)`. Model: opus-5-5
Author
Collaborator

What Go's server allows: an answer is possible. Go's HTTP/1 server reads up to 256 KiB of request body the handler left unread before it writes the first byte of an answer. It skips that read when the answer carries Connection: close, or after ResponseController.EnableFullDuplex.

What the visitor sees today, on next (aeaa9d1), with SWRP_MAX_REQUEST_TIME_MS=15000 and SWRP_MAX_RESPONSE_TIME_MS=300: the visitor announces 164 KiB, sends 65 KiB and stops; the app answers 200 at once. The visitor reads nothing for 15 seconds, then the connection closes with no bytes. The status write waits for the body until the request time limit ends that read, and by then its 10 seconds to be sent have passed.

Plan, in internal/rproxy:

  • When the app answers before the body has arrived whole, the answer goes out with Connection: close, so the visitor gets it at once. Go closes the connection after an answer sent before the whole body in its own cases too.
  • A second change is needed. Once the handler returns, Go's server reads the rest of the body with no deadline if another read of it is still waiting. Today that read is the transport's, so a visitor that stalls would hold the connection open for good. finish ends the transport's read first. Go's server then reads the rest until SWRP_MAX_REQUEST_TIME_MS runs out, and closes the connection.

Also found, outside this issue: an answer rproxy gives itself fails the same way. With the same settings, a deny to a visitor that stops sending its body gives no bytes after 15 seconds. It needs its own issue.

Model: opus-5-5

**What Go's server allows:** an answer is possible. Go's HTTP/1 server reads up to 256 KiB of request body the handler left unread before it writes the first byte of an answer. It skips that read when the answer carries `Connection: close`, or after `ResponseController.EnableFullDuplex`. **What the visitor sees today**, on `next` (`aeaa9d1`), with `SWRP_MAX_REQUEST_TIME_MS=15000` and `SWRP_MAX_RESPONSE_TIME_MS=300`: the visitor announces 164 KiB, sends 65 KiB and stops; the app answers 200 at once. The visitor reads nothing for 15 seconds, then the connection closes with no bytes. The status write waits for the body until the request time limit ends that read, and by then its 10 seconds to be sent have passed. **Plan**, in `internal/rproxy`: - When the app answers before the body has arrived whole, the answer goes out with `Connection: close`, so the visitor gets it at once. Go closes the connection after an answer sent before the whole body in its own cases too. - A second change is needed. Once the handler returns, Go's server reads the rest of the body with no deadline if another read of it is still waiting. Today that read is the transport's, so a visitor that stalls would hold the connection open for good. `finish` ends the transport's read first. Go's server then reads the rest until `SWRP_MAX_REQUEST_TIME_MS` runs out, and closes the connection. **Also found, outside this issue:** an answer `rproxy` gives itself fails the same way. With the same settings, a deny to a visitor that stops sending its body gives no bytes after 15 seconds. It needs its own issue. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/smallwebwaf#298