From 762d8964f52f750b28900b59e006f2a692309a45 Mon Sep 17 00:00:00 2001 From: mukktinaadh <159904553+mukktinaadh@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:37:50 +0530 Subject: [PATCH] fix(streaming): drain response body after [DONE] to prevent connection destruction Fixes #3440. After the SSE decoder yields [DONE], the underlying iter_bytes() / aiter_bytes() may not yet have delivered the HTTP/1.1 chunked transfer-encoding terminator (0\r\n\r\n) to h11. When response.close() is called in that state, h11's their_state is still SEND_RESPONSE, causing httpcore to destroy the connection (TCP FIN) instead of returning it to the pool (IDLE). This drain was originally present (7e2b2544, 2023-11) and was removed in 6132922c (2025-10-29). This commit restores it for both sync and async paths. --- src/openai/_streaming.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/openai/_streaming.py b/src/openai/_streaming.py index 45c13cc11d..7a9c92508d 100644 --- a/src/openai/_streaming.py +++ b/src/openai/_streaming.py @@ -61,6 +61,13 @@ def __stream__(self) -> Iterator[_T]: try: for sse in iterator: if sse.data.startswith("[DONE]"): + # Drain any remaining bytes from the response body so that + # h11 can parse the HTTP/1.1 chunked transfer-encoding + # terminator (0\r\n\r\n) and advance its state to DONE. + # Without this, response.close() destroys the connection + # (TCP FIN) instead of returning it to the pool. See #3440. + for _remaining in self.response.iter_bytes(): + pass break # we have to special case the Assistants `thread.` events since we won't have an "event" key in the data @@ -171,6 +178,10 @@ async def __stream__(self) -> AsyncIterator[_T]: try: async for sse in iterator: if sse.data.startswith("[DONE]"): + # Drain any remaining bytes so h11 parses the chunked + # terminator before we close. See #3440. + async for _remaining in self.response.aiter_bytes(): + pass break # we have to special case the Assistants `thread.` events since we won't have an "event" key in the data