Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/openai/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drain the existing event iterator

_iter_events() has already started response.iter_bytes() to produce the [DONE] event, and HTTPX marks the response stream consumed as soon as that underlying iterator is entered. Starting a second self.response.iter_bytes() here therefore raises httpx.StreamConsumed instead of ending a normal completed stream (the async branch has the same issue). Resume and drain iterator itself, as the prior implementation did.

Useful? React with 👍 / 👎.

pass
break

# we have to special case the Assistants `thread.` events since we won't have an "event" key in the data
Expand Down Expand Up @@ -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
Expand Down