Skip to content

fix(parsing): guard against None response.output in parse_response and streaming accumulator#3420

Open
Vitaliy-Pikalo wants to merge 2 commits into
openai:mainfrom
Vitaliy-Pikalo:fix/parse-response-null-output
Open

fix(parsing): guard against None response.output in parse_response and streaming accumulator#3420
Vitaliy-Pikalo wants to merge 2 commits into
openai:mainfrom
Vitaliy-Pikalo:fix/parse-response-null-output

Conversation

@Vitaliy-Pikalo

Copy link
Copy Markdown

Summary

When a backend (e.g. the Codex endpoint) sends response.output: null in the terminal response.completed SSE event, the SDK crashes with TypeError: 'NoneType' object is not iterable because parse_response() iterates response.output unconditionally.

This PR applies two complementary fixes:

1. Null guard in parse_response() (src/openai/lib/_parsing/_responses.py)

-    for output in response.output:
+    for output in (response.output or []):

Prevents the TypeError for any caller — streaming or non-streaming — when response.output is None.

2. Snapshot fallback in the streaming accumulator (src/openai/lib/streaming/responses/_responses.py)

When the response.completed payload has output=null, but the accumulator's snapshot already has the reconstructed output items from the preceding response.output_item.added/done events, use those items instead of losing the data:

         elif event.type == "response.completed":
-            self._completed_response = parse_response(
-                text_format=self._text_format,
-                response=event.response,
-                input_tools=self._input_tools,
-            )
+            completed_response = event.response
+            if completed_response.output is None and snapshot.output:
+                completed_response = construct_type_unchecked(
+                    type_=type(event.response),
+                    value={**event.response.to_dict(), "output": [item.to_dict() for item in snapshot.output]},
+                )
+            self._completed_response = parse_response(
+                text_format=self._text_format,
+                response=completed_response,
+                input_tools=self._input_tools,
+            )

Fix 1 alone prevents the crash but returns output=[], discarding all streamed items. Fix 2 preserves the actual content the user already received over the wire.

Fixes

Closes #3314
Closes #3321
Closes #3325

When the API sends `response.output: null` in a response.completed event,
parse_response() would raise TypeError: 'NoneType' object is not iterable.

Fixes openai#3314, openai#3321, openai#3325
@Vitaliy-Pikalo Vitaliy-Pikalo requested a review from a team as a code owner June 22, 2026 14:36

@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: f633aad112

ℹ️ 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".

if completed_response.output is None and snapshot.output:
completed_response = construct_type_unchecked(
type_=type(event.response),
value={**event.response.to_dict(), "output": [item.to_dict() for item in snapshot.output]},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid finalizing from partial snapshots

When response.completed has output: null, this serializes snapshot.output as if it were the final output, but the snapshot is only partially reconstructed: accumulate_event() folds in added items, content parts, text deltas, and function-call argument deltas, while other finalizing updates such as response.output_item.done (which carries the completed item/status) and custom-tool/code-interpreter/reasoning/annotation done or delta events are just passed through. In those streams, get_final_response() can now return stale or incomplete items, such as messages still marked in_progress or tool-call fields missing their final values, instead of the completed response.

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

1 participant