fix: clear the stream close claim between attempts so streaming retries and fallbacks work - #4911
Conversation
…es and fallbacks work
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe fix clears the streaming connection-closed context flag after a failed first-chunk attempt is drained, so subsequent retry or fallback streams start cleanly. Regression tests and a changelog entry were added. ChangesStreaming retry/fallback fix
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Hey the fix is at correct place can you also add it in function |
|
Done, added the clear to clearCtxForFallback as well. |
…es and fallbacks work (maximhq#4911) * fix: clear the stream close claim between attempts so streaming retries and fallbacks work * fix: also clear the stream close claim in clearCtxForFallback --------- Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
…es and fallbacks work (maximhq#4911) * fix: clear the stream close claim between attempts so streaming retries and fallbacks work * fix: also clear the stream close claim in clearCtxForFallback --------- Co-authored-by: Akshay Deo <akshay@akshaydeo.com>
Summary
Fixes #4788. Since v1.6.0, any streaming retry or fallback that follows a provider error embedded in an HTTP 200 SSE stream was dead on arrival: the new attempt's stream failed every read with
stream closedbefore delivering a single chunk, and the request ultimately failed with the primary error. The reporter hit this with an OpenAI-compatible primary falling back to DeepSeek's Anthropic-compatible endpoint, but the bug is not specific to either provider.When a stream dies through an SSE-embedded error,
CheckFirstStreamChunkForErrorrequalifies it as an attempt failure so retries and fallbacks can run. By then the dead stream's teardown has already runReleaseStreamingResponse, which since #4678 claims theconnection_closedflag on the request's shared context (an atomic guard against double-releasing the fasthttp body stream). That claim is scoped to the response it released, but the flag stayed set on the context. Nothing cleared it between attempts, so the idle-timeout reader of the next attempt's stream treated its own fresh stream as already closed and every read returnedstream closed. On v1.5.16 the guard was read-only, which is why the same setup worked there.Changes
executeRequestWithRetriesclearsBifrostContextKeyConnectionClosedwhen it requalifies a first-chunk error, right after the dead stream's teardown is known to be complete (<-drainDone). This covers both the in-loop retry and the fallback path, which re-entersexecuteRequestWithRetriesper attempt. Clearing is race-free at that point: the drain only completes after the provider goroutine's deferredReleaseStreamingResponse/CloseStreamhave run, and the cancellation watcher and idle timer are stopped before that.core/streamfallback_test.gocover both paths against localhttptestservers: an OpenAI-compatible primary that returns HTTP 200 with an SSE error event, with an Anthropic fallback that streams normally, and a single-provider retry where the first attempt fails the same way. Both fail before the fix (the fallback test fails with the primary error after the fallback stream dies, the retry test fails withError reading stream: stream closed) and pass with it.Cancellation semantics are unchanged: a genuinely cancelled request surfaces
RequestCancelled, which neither retries nor falls back, and non-streaming requests never take this branch.Type of change
Affected areas
How to test
Both tests fail on
devwithout this change and pass with it.go build ./...passes and the package tests (go test .) are green, including under the race detector. The pre-existingTestResponsesMessageToolCallArguments/real_tool_search_call_frames_from_openaifailure incore/schemasfails ondevwithout this change as well.Screenshots/Recordings
N/A
Breaking changes
Related issues
Closes #4788
Security considerations
None. The flag only coordinates stream teardown ownership within a single request.
Checklist
docs/contributing/README.mdand followed the guidelines