Skip to content

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

Description

@HardMax71

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

Describe the bug

_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, _streaming.py iterates the response with no handling at all, so a read timeout or a dropped connection mid-stream surfaces as the raw httpx.ReadTimeout / httpx.RemoteProtocolError (httpx2.* on 3.x). That is not an APIError, and max_retries is not consulted.

So except openai.APIError around a streaming call misses the most common streaming failure, and a client with max_retries=2 makes exactly one request.

Expected: the same wrapping as the initial send, APITimeoutError / APIConnectionError, so the exception hierarchy holds for the whole call. Whether a partially consumed stream can be retried is a separate question; wrapping alone fixes the catch side. anthropic-sdk-python has the same gap, same generated base: anthropics/anthropic-sdk-python#1919

To Reproduce

  1. Run the script below. No network, the mock transport yields one chunk and then raises ReadTimeout.
  2. The raw httpx.ReadTimeout escapes, isinstance(e, openai.APIError) is False, one request was made.

Code snippets

import platform
import sys

import openai

try:
    import httpx2 as httpx
except ImportError:
    import httpx

FIRST = b'data: {"id":"c1","object":"chat.completion.chunk","created":0,"model":"gpt-5.2","choices":[{"index":0,"delta":{"role":"assistant","content":"hi"},"finish_reason":null}]}\n\n'
requests_seen = 0


class DiesMidStream(httpx.SyncByteStream):
    def __iter__(self):
        yield FIRST
        raise httpx.ReadTimeout("timed out while reading the stream")


def handler(request):
    global requests_seen
    requests_seen += 1
    return httpx.Response(200, headers={"content-type": "text/event-stream"}, stream=DiesMidStream())


client = openai.OpenAI(api_key="x", http_client=httpx.Client(transport=httpx.MockTransport(handler)), max_retries=2)
print(f"python {sys.version.split()[0]} {platform.system()}, openai {openai.__version__}, httpx {httpx.__version__}")
try:
    for _ in client.chat.completions.create(model="gpt-5.2", messages=[{"role": "user", "content": "hi"}], stream=True):
        pass
except Exception as e:  # noqa: BLE001
    print(f"escaped: {type(e).__module__}.{type(e).__name__}: {e}")
    print(f"isinstance(e, openai.APIError) = {isinstance(e, openai.APIError)}")
print(f"requests made with max_retries=2: {requests_seen}")

Output (python 3.12.13, macOS):

openai 2.26.0, httpx 0.28.1
escaped: httpx.ReadTimeout: timed out while reading the stream
isinstance(e, openai.APIError) = False
requests made with max_retries=2: 1

openai 3.8.0, httpx 2.12.0
escaped: httpx2.ReadTimeout: timed out while reading the stream
isinstance(e, openai.APIError) = False
requests made with max_retries=2: 1

OS

macOS

Python version

Python v3.12.13

Library version

openai v2.26.0 and v3.8.0

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions