Skip to content

Streaming response body can be truncated when the program ends right after close out #680

Description

@logbie

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

IN WORKPresently working on thisbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions