Repository navigation
fix: read a streamed reply by its first bytes, not only its content-type - #165
Merged
Merged
Conversation
Parasail labels its SSE streams application/json, so every streamed call was buffered whole, failed JSON parsing as an internal 500, and three of them cooled the account for every caller of every model on it. A stream request's 2xx body that opens with `data:` is now relayed as SSE; JSON answers and error statuses stay buffered as before.
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.
Closes #164.
Cause
HttpTransport::sendchose the stream path fromcontent-typealone. Parasail answersstream: truewithcontent-type: application/jsonand a standard SSE body:The transport read the whole stream as one body,
parse_jsonfailed ondata:as an internal 500, and that 500 counts as an account fault. Afterfailure_thresholdsuch calls, the account cooled down for every model on it, including non-streamed calls that work.Fix
For a stream request without an SSE
content-type, the transport reads the first chunk:data:is relayed as SSE, with that chunk put back in front.Non-stream requests and SSE-labelled responses take the code path they took before. The health rule stays as it is: a 200 the gateway cannot parse is still an account fault. With the stream read correctly, Parasail no longer trips it.
Other providers
Only the OpenAI chat, Anthropic, Responses, Gemini (
alt=sse), DashScope native and Bedrock engines send stream requests. Every one of those vendors labels its streamtext/event-streamorapplication/vnd.amazon.eventstream, so none of them reaches the new branch. A vendor that answers a stream request with JSON (for example a 200 error envelope) still gets a JSON body. The only difference on that path is the read-error message, which is nowupstream stream failed: …. The status (502) and error code are unchanged.Hot path
Zero for correctly labelled streams and for non-stream calls. Only a mislabelled stream pays one extra boxed stream adapter. A JSON answer to a stream request is copied once into the buffer.
A/B through a real HTTP hop (front gateway → mock-mode gateway as upstream), main vs this branch, interleaved with the order swapped, 10 s each:
Evidence
parse openai response. After the third, both models return 503no healthy upstream accountfor streamed and non-streamed calls alike. This reproduces both symptoms in the issue.a_stream_request_body_is_read_by_its_first_bytesfails on main (the SSE case comes back as JSON) and passes here. It also covers a JSON answer and an SSE-shaped error status.cargo fmt --check,cargo clippy --workspace --all-targets -D warnings: clean.cargo test --workspacewith real Postgres 17 and Redis 7: 764 passed, 0 failed, 4 ignored. The Postgres- and Redis-backed suites ran: the test databases were created and Redis keys were written.Second commit: TLS advisories
cargo denystarted failing on two RustSec advisories published after main's last CI run, so main fails the same check today:rustls-webpkimay accept distrusted intermediates.webpki-rootsbundles the distrusted emSign roots.Cargo.lockmovesrustls-webpki0.103.15 → 0.103.16 andwebpki-roots1.0.8 → 1.0.10. The 0.26.11 shim follows the 1.x line.Checks after the bump:
cargo deny check: advisories, bans, licenses and sources all ok.cargo test --workspacegives 764 passed, 0 failed, 4 ignored.Known limitation
The check reads exactly one chunk, and
bytes_stream()does not guarantee where chunks end. A mislabelled stream whose first chunk ends beforedata:is complete, or that opens with a comment line (: keep-alive), a bare newline orevent:, is still buffered as JSON and fails the way it does on main. Correctly labelled streams and non-stream calls never reach this check.No current vendor has shown this: every live Parasail stream delivered a complete
data: {...}in its first chunk over HTTP/2. If a mislabelled vendor that does show it turns up, widen the prefix check and read across chunks within the same deadline.Size
Production +20 lines, comments +0, tests +45.
docs/providers.mdnotes the rule and lists Parasail among the OpenAI-compatible vendors.