-
Notifications
You must be signed in to change notification settings - Fork 5.1k
fix: drain remaining bytes after [DONE] before closing response (#3440) #3520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,19 @@ def __stream__(self) -> Iterator[_T]: | |
| try: | ||
| for sse in iterator: | ||
| if sse.data.startswith("[DONE]"): | ||
| # Drain remaining events from the existing iterator so the | ||
| # underlying response.iter_bytes() reaches EOF, allowing | ||
| # h11 to advance to DONE state before close. Without this, | ||
| # response.close() sends TCP FIN while the chunked terminator | ||
| # (0\r\n\r\n) is still in flight, causing connection pool | ||
| # degradation and proxy errors. (#3440) | ||
| # | ||
| # We must drain through `iterator` (not start a new | ||
| # `self.response.iter_bytes()`) because httpx only allows | ||
| # one active iterator at a time — a second call raises | ||
| # `httpx.StreamConsumed`. | ||
| for _ in iterator: | ||
| pass | ||
|
Comment on lines
+76
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a custom or misbehaving SSE endpoint sends Useful? React with 👍 / 👎. |
||
| break | ||
|
|
||
| # we have to special case the Assistants `thread.` events since we won't have an "event" key in the data | ||
|
|
@@ -172,6 +185,19 @@ async def __stream__(self) -> AsyncIterator[_T]: | |
| try: | ||
| async for sse in iterator: | ||
| if sse.data.startswith("[DONE]"): | ||
| # Drain remaining events from the existing iterator so the | ||
| # underlying response.aiter_bytes() reaches EOF, allowing | ||
| # h11 to advance to DONE state before close. Without this, | ||
| # response.aclose() sends TCP FIN while the chunked terminator | ||
| # (0\r\n\r\n) is still in flight, causing connection pool | ||
| # degradation and proxy errors. (#3440) | ||
| # | ||
| # We must drain through `iterator` (not start a new | ||
| # `self.response.aiter_bytes()`) because httpx only allows | ||
| # one active iterator at a time — a second call raises | ||
| # `httpx.StreamConsumed`. | ||
| async for _ in iterator: | ||
| pass | ||
| break | ||
|
|
||
| # we have to special case the Assistants `thread.` events since we won't have an "event" key in the data | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an upstream/proxy has already delivered the
[DONE]sentinel but then stalls or closes before EOF/chunk termination, this new drain keeps reading and anyReadTimeout/RemoteProtocolErrorfrom the best-effort cleanup now escapes after the stream is logically complete. Before this change the stream ended at[DONE]and thefinallyblock just closed the response, so users would not see a failure after receiving the complete stream; the async drain has the same issue. Consider making post-[DONE]draining best-effort so cleanup failures do not replace successful stream completion.Useful? React with 👍 / 👎.