Skip to content

fix(streaming): wrap mid-stream transport errors as APITimeoutError/APIConnectionError - #3818

Open
adhavan18 wants to merge 1 commit into
openai:mainfrom
adhavan18:fix-streaming-transport-error-wrapping
Open

fix(streaming): wrap mid-stream transport errors as APITimeoutError/APIConnectionError#3818
adhavan18 wants to merge 1 commit into
openai:mainfrom
adhavan18:fix-streaming-transport-error-wrapping

Conversation

@adhavan18

Copy link
Copy Markdown
  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

_base_client wraps the initial send — httpx.TimeoutException becomes APITimeoutError, other transport errors become APIConnectionError, and both go through the retry loop. Once the response is streaming, Stream.__stream__ / AsyncStream.__stream__ in _streaming.py iterated the response with no exception handling at all, so a read timeout or a dropped connection mid-stream surfaced as the raw httpx.ReadTimeout / httpx.RemoteProtocolError, not an openai.APIError.

That means except openai.APIError around a streaming call misses the most common streaming failure, and max_retries is never consulted for it.

The fix wraps the iteration loop in both __stream__ methods with the same pattern _base_client.py already uses for the initial request: timeout_exceptions()APITimeoutError, an already-raised OpenAIError (the in-stream error-event case a few lines up) re-raised as-is so it isn't double-wrapped, anything else → APIConnectionError. Whether a partially-consumed stream can itself be retried is a separate question — this only fixes the catch side, matching what the non-streaming path already does.

Also fixed in anthropic-sdk-python (same generated base, same gap): anthropics/anthropic-sdk-python#1919

Additional context & links

Closes #3811

Verified against the repro script from the issue (mock transport that yields one chunk then raises ReadTimeout): before the fix, isinstance(e, openai.APIError) is False; after, it raises APITimeoutError and isinstance is True.

Added test_sync_stream_wraps_mid_stream_transport_error and test_async_stream_wraps_mid_stream_transport_error to tests/test_streaming.py — both confirmed to fail on the unfixed code (raw httpx2.ReadTimeout escapes) and pass with the fix. Full tests/test_streaming.py suite (22 tests) passes. The broader test run has unrelated pre-existing collection errors from missing optional dev deps (rich, botocore, etc.) in this environment — not touched by this change.

…PIConnectionError

_base_client wraps the initial send so httpx transport failures surface as
APIError subclasses, but Stream/AsyncStream.__stream__ iterated the response
with no handling at all. A read timeout or dropped connection mid-stream
escaped as a raw httpx exception, so `except openai.APIError` around a
streaming call missed the most common streaming failure and max_retries was
never consulted for it.

Wraps the iteration in the same try/except pattern _base_client already
uses: timeout_exceptions() -> APITimeoutError, an intentionally-raised
OpenAIError (the in-stream error-event case) re-raised as-is, anything else
-> APIConnectionError.
@adhavan18
adhavan18 requested a review from a team as a code owner September 8, 2026 05:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5502eb26b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/openai/_streaming.py
Comment on lines +110 to +111
except timeout_exceptions() as err:
raise APITimeoutError(request=response.request) from err

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 Preserve timeout callbacks for Assistant streams

When an Assistants stream raises httpx2.ReadTimeout, this conversion means AssistantEventHandler.__stream__ and its async counterpart no longer match their _timeout_exceptions() branches, so they invoke only on_exception() and skip on_timeout(). This also changes the exception exposed by until_done() and breaks the existing sync and async callback expectations in tests/test_httpx2.py::test_assistant_stream_timeout_callbacks_preserve_httpx2_family; recognize APITimeoutError in those handlers (or preserve the transport exception for that path).

Useful? React with 👍 / 👎.

Comment thread src/openai/_streaming.py
Comment on lines +114 to +115
except Exception as err:
raise APIConnectionError(request=response.request) from err

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 Limit connection wrapping to transport errors

This catch also converts exceptions unrelated to the connection into APIConnectionError: malformed SSE data can raise UnicodeDecodeError or JSONDecodeError, and custom stream implementations can raise ordinary application exceptions. That changes their semantics and already makes the sync and async interruption checks in tests/test_sse_framing.py fail because the expected RuntimeError is replaced; restrict this branch to the HTTPX/HTTPX2 transport-error families so only the intended mid-stream failures are wrapped.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transport errors while consuming a stream escape as raw httpx exceptions instead of APITimeoutError / APIConnectionError

1 participant