Summary
close out on a server response stream ends the body by dropping the sender. That signals the end; it does not wait for the terminating chunk to reach the client. If the program finishes immediately afterwards, interpreter teardown and the runtime drop race the connection task that is still flushing — and the client sees a truncated body instead of a clean end of stream.
Surfaced as a flaky CI failure, not a hypothesis about code that never misbehaves:
test_early_chunk_is_visible_before_the_body_completes
stream transport failed instead of ending cleanly: error decoding response body
Evidence that it is a race, not a deterministic bug
On a single commit (c8a9612, PR #676) in one CI run:
| Job |
Command |
Result |
| Build, Test, Clippy |
cargo test --workspace |
passed |
| Integration Tests (Linux) |
cargo test --test '*' |
failed on this test |
Same code, same test, two outcomes. Locally it is 8/8 clean in isolation and clean across two full-suite runs, so it needs a loaded machine to show up.
Reproduction
tests/response_stream_backpressure_test.rs::test_early_chunk_is_visible_before_the_body_completes. The handler is:
listen on port <port> as srv
main loop:
wait for request comes in on srv as req with timeout 30000
start streaming response to req with status 200 and content type "text/plain" as out
write chunk "EARLY" to out
flush out
wait for 2000 milliseconds
write chunk "LATE" to out
close out
break
end loop
Note there is no close server srv — the loop simply breaks.
Mechanism
Verified: close out is implemented by removing the handle from server_response_streams, i.e. dropping the sender — see IoClient::close_response_streams (src/interpreter/mod.rs), whose doc comment says "Close (drop the sender for) each server response stream … ending its body so the client stops waiting."
Inferred from that, and consistent with the observed symptom: nothing awaits the body actually reaching the wire. After close out the sequence is break → main loop exits → interpret() returns → the host drops the tokio Runtime → warp's in-flight connection task is aborted. If the terminating chunk has not been written yet, the client gets a truncated chunked body and reports a decode error rather than clean EOF.
The 2 s wait in this particular test is incidental; it just widens the window by ensuring the connection is still live and mid-body when the program ends.
Why this is a product concern and not just a flaky test
Per the binding testing policy (testing.md §11.3), streaming and lifecycle work must prove clean shutdown and that a closed stream is actually delivered. "The sender was dropped" is not the same as "the client received the body." Any WFL program whose last act is to stream a response and then finish — a one-shot CLI-style server, a webhook responder, a break after serving — can lose the tail of its own response, and would look to the client like a network error.
Deliberately not fixed by adding a sleep to the test: that would be manufactured green under §8.2 and would hide the real gap.
Suggested direction
Give stream close an acknowledgement path rather than fire-and-forget — e.g. have close out await confirmation that the body was flushed/completed, and have interpreter shutdown drain outstanding response streams before returning. The existing graceful-shutdown handling for close server (TestPrograms/web_server_graceful_shutdown_test.wfl) is the natural place to look for the pattern to reuse.
A regression test should assert the client reads a complete body when the program ends immediately after close out, and must fail against today's behaviour on a loaded machine (Red evidence per §3/§6).
Context
Found while working PR #676 (issues #664–#667). Unrelated to that PR's changes — no database, crypto, TOML or filesystem code is on this path — so it is filed separately rather than folded in. Streaming responses landed in #641.
Summary
close outon a server response stream ends the body by dropping the sender. That signals the end; it does not wait for the terminating chunk to reach the client. If the program finishes immediately afterwards, interpreter teardown and the runtime drop race the connection task that is still flushing — and the client sees a truncated body instead of a clean end of stream.Surfaced as a flaky CI failure, not a hypothesis about code that never misbehaves:
Evidence that it is a race, not a deterministic bug
On a single commit (
c8a9612, PR #676) in one CI run:cargo test --workspacecargo test --test '*'Same code, same test, two outcomes. Locally it is 8/8 clean in isolation and clean across two full-suite runs, so it needs a loaded machine to show up.
Reproduction
tests/response_stream_backpressure_test.rs::test_early_chunk_is_visible_before_the_body_completes. The handler is:Note there is no
close server srv— the loop simplybreaks.Mechanism
Verified:
close outis implemented by removing the handle fromserver_response_streams, i.e. dropping the sender — seeIoClient::close_response_streams(src/interpreter/mod.rs), whose doc comment says "Close (drop the sender for) each server response stream … ending its body so the client stops waiting."Inferred from that, and consistent with the observed symptom: nothing awaits the body actually reaching the wire. After
close outthe sequence isbreak→ main loop exits →interpret()returns → the host drops the tokioRuntime→ warp's in-flight connection task is aborted. If the terminating chunk has not been written yet, the client gets a truncated chunked body and reports a decode error rather than clean EOF.The 2 s wait in this particular test is incidental; it just widens the window by ensuring the connection is still live and mid-body when the program ends.
Why this is a product concern and not just a flaky test
Per the binding testing policy (
testing.md§11.3), streaming and lifecycle work must prove clean shutdown and that a closed stream is actually delivered. "The sender was dropped" is not the same as "the client received the body." Any WFL program whose last act is to stream a response and then finish — a one-shot CLI-style server, a webhook responder, abreakafter serving — can lose the tail of its own response, and would look to the client like a network error.Deliberately not fixed by adding a sleep to the test: that would be manufactured green under §8.2 and would hide the real gap.
Suggested direction
Give stream close an acknowledgement path rather than fire-and-forget — e.g. have
close outawait confirmation that the body was flushed/completed, and have interpreter shutdown drain outstanding response streams before returning. The existing graceful-shutdown handling forclose server(TestPrograms/web_server_graceful_shutdown_test.wfl) is the natural place to look for the pattern to reuse.A regression test should assert the client reads a complete body when the program ends immediately after
close out, and must fail against today's behaviour on a loaded machine (Red evidence per §3/§6).Context
Found while working PR #676 (issues #664–#667). Unrelated to that PR's changes — no database, crypto, TOML or filesystem code is on this path — so it is filed separately rather than folded in. Streaming responses landed in #641.