fix(sandbox): stop splitting Cloudflare SSE events at chunk boundaries - #4215
Open
abhay-codes07 wants to merge 1 commit into
Open
fix(sandbox): stop splitting Cloudflare SSE events at chunk boundaries#4215abhay-codes07 wants to merge 1 commit into
abhay-codes07 wants to merge 1 commit into
Conversation
The Cloudflare line decoder classified a chunk that ends on CR immediately: it emitted the line and kept a bare CR as the buffer. A CR at the end of the available bytes cannot be classified yet, because the next chunk may start with LF and CRLF is a single terminator. The held CR was then read as the start of a new line, so the following LF produced a blank line. A blank line dispatches an SSE event, so one multi-line event arrived as several. Transport chunk boundaries are arbitrary, so this depended on how the Worker response happened to be framed rather than on its content. flush() had the matching problem and appended a blank line for a stream ending on CR, and the CR-only and trailing-CR shapes were wrong even unchunked. Hold the unterminated line, CR included, until the next chunk decides, matching the httpx decoder this was adapted from and str.splitlines(). Before the change six of seven stream shapes disagreed with splitlines and 191 of 400 randomized chunkings changed the events produced; both are now zero.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Cloudflare Worker's server-sent event stream is split into lines by a hand-rolled decoder that classifies a chunk ending on CR immediately: it emits the line and keeps a bare CR as the buffer. A CR at the end of the available bytes cannot be classified yet, because the next chunk may begin with LF and CRLF is a single terminator. The stored CR is then read as the start of a new line, so the following LF produces a blank line, and a blank line dispatches an SSE event. One multi-line event arrives as several.
Transport chunk boundaries are arbitrary, so which events an application receives depended on how the response happened to be framed rather than on its content:
flush()had the matching problem and appended a blank line for a stream ending on CR. The CR-only and trailing-CR shapes were also wrong when decoded in a single call, independently of chunking.The decoder is adapted from the httpx line decoder, which defers a trailing CR into the next iteration and documents that it matches
str.splitlines(). This restores that behavior: keep the unterminated line, CR included, until the next chunk decides, and letflushclose the pending line instead of adding a blank one.Measured against
str.splitlines()as the oracle:Test plan
The decoders had no tests, which is how this survived. Added to
tests/extensions/sandbox/test_cloudflare.py:test_sse_line_decoder_matches_splitlines_for_every_chunk_boundary, parametrized over LF, CRLF, CR, mixed, empty-data, trailing-CR and bare-CRLF streams, asserting agreement withstr.splitlines()at every chunk size from 1 to the full length.test_sse_event_is_not_split_when_crlf_straddles_a_chunk_boundary, asserting the decoded events are identical for every chunk size.test_sse_line_decoder_flush_does_not_emit_a_phantom_line_for_a_trailing_cr.8 fail on
mainand pass with the fix. The LF-only case passes in both runs, which is the control confirming the tests are not simply rejecting the decoder wholesale:Verification from the repository root:
make formatmake lintmake mypymain, none in the touched filesmake pyrightmain(src/agents/sandbox/util/tar_utils.py:161)uv run pytest tests/extensions/sandbox/test_cloudflare.pymake testsThe full suite run was done on Windows, where some sandbox symlink and tracing timing tests fail independently of this change. I diffed the failing set against a clean
maincheckout in the same environment and the two sets are identical, 54 versus 54, with no differences in either direction.Issue number
Closes #4214
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PRThe verification script is a bash script that shells out to
make. I ran the underlying steps individually instead, with the results above.