fix(server): do not append [DONE] after an upstream in-band stream error - #334
Open
enwaiax wants to merge 1 commit into
Open
fix(server): do not append [DONE] after an upstream in-band stream error#334enwaiax wants to merge 1 commit into
enwaiax wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (3)
WalkthroughThe translation encoder now reports whether an in-band error ended the stream. The server passes this outcome to SSE framing. OpenAI Chat framing suppresses ChangesStream outcome propagation
Estimated code review effort: 3 (Moderate) | ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
An upstream SSE error event is a well-formed JSON event, so it flows through frame_event() as ordinary data and leaves the framing loop's `failed` flag unset. The OpenAI Chat sentinel is then appended to a stream that did not complete, and an SDK client that stops at [DONE] reports the truncated, failed turn as a successful completion. The translation layer already detects this: encode_stream() checks state.errored and returns early, deliberately skipping codec.finish(). That outcome was simply never communicated to the serving layer, which cannot otherwise distinguish a clean EOF from an early stop. Expose it via StreamOutcome and consult it before emitting the sentinel. encode_stream() keeps its signature and delegates to the new encode_stream_with_outcome(), so existing callers are unaffected. Only the Chat leg was affected: [DONE] is Chat-specific, and the terminal events for Anthropic Messages and OpenAI Responses come from codec.finish(), which the early return already skips. Signed-off-by: enwaiax <32839114+enwaiax@users.noreply.github.com>
enwaiax
force-pushed
the
xiangw/fix-sse-done-after-error
branch
from
August 8, 2026 02:33
b64e2e9 to
ce68902
Compare
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.
Problem
When an upstream emits an SSE error event mid-stream,
/v1/chat/completionsforwards that error frame and then still appends
data: [DONE]— the OpenAI Chatsuccess sentinel. An SDK client that stops at
[DONE]treats the truncated,failed turn as a normally completed answer.
Captured against the release binary (loopback upstream, one content frame followed
by one error frame):
/v1/messagesand/v1/responsesare unaffected — neither uses a[DONE]sentinel, and both already terminate on the error event.
Root cause
crates/switchyard-server/src/sse.rstracks a localfailedflag, but only aframing/JSON error or a transport-level stream error sets it. An upstream in-band
error event is a well-formed JSON event, so it flows through
frame_event()asordinary data and leaves
failed == false:The translation layer already knows the stream failed:
encode_stream()checksstate.erroredand returns early, deliberately skippingcodec.finish(). Thatoutcome was simply never communicated to the serving layer, which cannot otherwise
tell a clean EOF from an early stop.
This also matches the contract already stated in
crates/switchyard-translation/src/helpers.rs: "An in-band error is terminal forevery target format: the encoder emits the pre-error content and the error, then
drops any later chunk."
Fix
Expose the outcome across the layer boundary and consult it before emitting the
sentinel.
[DONE]stays in the serving layer — framing is the serving layer's jobper the
RawEventStreamdoc contract, and[DONE]is not a JSON event object, soit does not belong in a codec.
StreamOutcome(a sharedAtomicBool) set whereencode_streamalreadyreturns early on
state.erroredencode_stream_with_outcome();encode_stream()keeps its signature anddelegates to it, so existing callers (e.g.
libsy-llm-client) are untouchedframe_stream()skips the sentinel when the outcome reports an in-band errorTests
Two new unit tests in
sse.rs:upstream_in_band_error_suppresses_the_done_marker— the reported defectclean_stream_still_emits_the_done_marker— guards the OpenAI Chat contract forthe success path
The pre-existing
stream_error_terminates_without_done_markerstill passes.Verification
End-to-end A/B against the same loopback upstream, same test, only the binary
swapped:
Note
The Python
switchyard serveentry point has the same defect(
switchyard/lib/endpoints/sse_helpers.pyyields[DONE]unless an exception israised, and an upstream error event is not an exception). It has no equivalent
erroredsignal to read, so it needs its own fix rather than a port of this one —not included here to keep this PR to a single component.
Summary by CodeRabbit