From be738928f85870a08bb69a70b96b52c595596c83 Mon Sep 17 00:00:00 2001 From: nicha16 Date: Wed, 1 Jul 2026 18:17:28 +0700 Subject: [PATCH] fix(lib): guard parse_response against response.output=None parse_response iterated response.output directly, raising TypeError('NoneType' object is not iterable) when a completed Response has output=None (incomplete/failed/reasoning-only responses, and some proxy/aggregator backends). Because this fires inside the streaming accumulator (accumulate_event -> parse_response), it crashes the whole stream rather than surfacing as a handleable result. Guard with (response.output or []). Adds a regression test that reproduces the crash without the guard. Fixes #3459 --- src/openai/lib/_parsing/_responses.py | 5 ++- .../test_parse_response_none_output.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/lib/responses/test_parse_response_none_output.py diff --git a/src/openai/lib/_parsing/_responses.py b/src/openai/lib/_parsing/_responses.py index 232718cef6..5a1b76c4c6 100644 --- a/src/openai/lib/_parsing/_responses.py +++ b/src/openai/lib/_parsing/_responses.py @@ -58,7 +58,10 @@ def parse_response( ) -> ParsedResponse[TextFormatT]: output_list: List[ParsedResponseOutputItem[TextFormatT]] = [] - for output in response.output: + # `response.output` can be None for incomplete/failed/reasoning-only + # responses (and via some proxy/aggregator backends); guard so the stream + # parser yields an empty output instead of raising TypeError. + for output in (response.output or []): if output.type == "message": content_list: List[ParsedContent[TextFormatT]] = [] for item in output.content: diff --git a/tests/lib/responses/test_parse_response_none_output.py b/tests/lib/responses/test_parse_response_none_output.py new file mode 100644 index 0000000000..def3255847 --- /dev/null +++ b/tests/lib/responses/test_parse_response_none_output.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from openai._types import omit +from openai.lib._parsing._responses import parse_response +from openai.types.responses import Response + + +def _minimal_response(**overrides: object) -> Response: + # Build a Response with the required scalar fields set; `output` is the + # field under test. Uses construct() to avoid pulling a full live payload. + base: dict[str, object] = dict( + id="resp_test", + created_at=0.0, + error=None, + incomplete_details=None, + instructions=None, + metadata=None, + model="gpt-4o-mini", + object="response", + output=[], + parallel_tool_calls=True, + temperature=1.0, + tool_choice="auto", + tools=[], + top_p=1.0, + ) + base.update(overrides) + return Response.construct(**base) + + +def test_parse_response_handles_none_output() -> None: + # `output` can be None for incomplete/failed/reasoning-only responses and + # via some proxy/aggregator backends. parse_response must not raise + # TypeError('NoneType' object is not iterable) — it should yield empty output. + response = _minimal_response(output=None) + + parsed = parse_response(text_format=omit, input_tools=None, response=response) + + assert parsed.output == []