Skip to content

fix(responses): reject with OpenAIError when the stream emits an error event - #2021

Open
morgan-coded wants to merge 1 commit into
openai:mainfrom
morgan-coded:responses-error-frame
Open

fix(responses): reject with OpenAIError when the stream emits an error event#2021
morgan-coded wants to merge 1 commit into
openai:mainfrom
morgan-coded:responses-error-frame

Conversation

@morgan-coded

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

Changes being requested

Fixes #2020

An API error frame reaching ResponseStream rejects finalResponse() with the raw frame as a plain object — instanceof OpenAIError is false — on both the live path (the SSE bare-data arm only converts on a data.error key, which {"type":"error",...} doesn't carry) and the fromReadableStream replay path. .on('error') listeners receive the same raw frame, and the declared event type asserted it (ResponseEvents mapped error to the raw ResponseErrorEvent).

The fix is a real case 'error': in #addEvent that throws new APIError(undefined, event, event.message, undefined) — the call shape core/streaming.ts already uses for its conversions. APIError extends OpenAIError and reads code/param straight off the frame, keeping the whole frame on .error. Throwing routes through EventStream's own error path (its _emit('error', …) contract allows only #handleError to call it), so finalResponse() and .on('error') observe the same converted object — the regression test asserts identity, plus message and code. Red on main (Received constructor: Object), green with the fix; full suite 1463 passing, snapshots untouched.

One line beyond the runtime fix: 'error' joins the ResponseEvents Omit list (alongside the two delta events there for the same reason), so .on('error') is typed OpenAIError. It's a narrowing — only code relying on the raw-frame behavior would notice — and easy to drop if you'd rather keep the type surface untouched.

Additional context & links

Two adjacent things deliberately left out; flag if you'd like either:

  • The transport-layer alternative: core/streaming.ts carries a TODO ("Is this where the error should be thrown?") in the arm these frames pass through — converting there would cover every consumer but widens the blast radius to Chat Completions.
  • An error frame arriving before response.created never reaches this switch: ResponseAccumulator throws its own generic error first, losing the server's message and code. Happy to follow up separately.

@morgan-coded
morgan-coded requested a review from a team as a code owner July 26, 2026 18:23

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +155 to +159
case 'error': {
// The API reports failures with an `error` event instead of a non-2xx response, so
// convert it here. Throwing hands it to `EventStream`'s error path, which rejects
// `finalResponse()` and notifies `error` listeners with the converted error.
throw new APIError(undefined, event, event.message, undefined);

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 Convert initial error events before snapshot accumulation

When the server sends an error frame before response.created, this branch is never reached because accumulateResponse() first throws its generic “expected 'response.created'” OpenAIError. Consequently finalResponse() and error listeners lose the server's message, code, and parameter and do not receive the promised APIError. Handle event.type === 'error' before calling the accumulator.

Useful? React with 👍 / 👎.

Comment on lines +155 to +159
case 'error': {
// The API reports failures with an `error` event instead of a non-2xx response, so
// convert it here. Throwing hands it to `EventStream`'s error path, which rejects
// `finalResponse()` and notifies `error` listeners with the converted error.
throw new APIError(undefined, event, event.message, undefined);

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 Retain converted errors for the async iterator

When a for await consumer is still processing the raw error frame before requesting its next item, this throw reaches the iterator's error listener while readQueue is empty. That listener marks the iterator done but does not retain the failure, so the subsequent next() returns cleanly with done: true instead of rejecting with this APIError; an await in the loop body makes this especially likely. Convert the frame before emitting it through event, or store the failure so the next iterator read rejects.

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.

ResponseStream rejects with a plain object when the API emits an error event

1 participant