Skip to content

fix: return api error instead of decode failure for non-object error bodies - #914

Open
andreynering wants to merge 1 commit into
openai:mainfrom
andreynering:fix/api-error-unmarshal-fallback
Open

fix: return api error instead of decode failure for non-object error bodies#914
andreynering wants to merge 1 commit into
openai:mainfrom
andreynering:fix/api-error-unmarshal-fallback

Conversation

@andreynering

Copy link
Copy Markdown
Contributor

Problem

When an HTTP error response's body is not the expected object shape, the SDK returns the raw json.UnmarshalTypeError instead of an *apierror.Error:

json: cannot unmarshal string into Go value of type apierror.Error

This happens for:

  • {"error": "you must provide a model parameter"} — a string error value, which non-OpenAI-compatible intermediaries sometimes return
  • entirely non-JSON bodies, e.g. an HTML "Bad Gateway" page from a proxy

The early return err in internal/requestconfig also discards the status code, headers, and response body, so callers can't distinguish a 400 from a 500, and retry decisions keyed off StatusCode (both in the SDK ecosystem and in downstream wrappers) are broken — a 5xx served in an unexpected shape becomes a permanent, unactionable error.

Solution

Always return *apierror.Error for status >= 400. When the error payload is missing or doesn't unmarshal into the expected object, fall back to putting the raw response body in Message (which would otherwise be empty) and return the fully populated error. StatusCode, Request, and Response are already set on the struct before the unmarshal attempt, so they are preserved as-is.

This also keeps errors.As(err, &apiErr) working for downstream consumers that only handle *openai.Error.

Testing

Added apierror_body_test.go (handwritten, so it doesn't affect the custom-code budget) covering:

  • string error value → *openai.Error with status code and raw body in Message
  • non-JSON (HTML) body → same fallback
  • well-formed object error → unchanged parsing behavior (regression guard)

@andreynering
andreynering requested a review from a team as a code owner September 10, 2026 18:56
@andreynering

Copy link
Copy Markdown
Contributor Author

Sorry for the AI generated PR description. I intended to open it manually but my agent decided to do it itself. Well...

Anyway, patch is important. 🙂 In summary, if the server decides to return something that is not JSON, like 502 (gateway timeout), <!html>... we still want the SDK to return a proper error type instead of a generic JSON parser error.

@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: 184d3c86f2

ℹ️ 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 thread internal/requestconfig/requestconfig.go

@sylvesterkaczmarek sylvesterkaczmarek 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.

apijson.UnmarshalRoot accepts JSON null for a root struct, so {"error":null} makes UnmarshalJSON return nil and bypasses this fallback. Treat a null/non-object error value as fallback before unmarshalling, and add a {"error":null} regression asserting the typed API error preserves the response status/body.

…bodies

When an HTTP error response's body isn't the expected object shape (e.g. a
missing, null, or string error value, or a non-JSON document entirely), the
SDK returned the raw json.UnmarshalTypeError and dropped the status code and
response. Always return *apierror.Error for status >= 400, falling back to
the raw body in Message. RawJSON() and Error() fall back to Message so the
raw diagnostics are also surfaced through the standard accessors.
@andreynering
andreynering force-pushed the fix/api-error-unmarshal-fallback branch from 184d3c8 to 2bf06cc Compare September 11, 2026 17:28
@andreynering

Copy link
Copy Markdown
Contributor Author

I just pushed an update.

@sylvesterkaczmarek sylvesterkaczmarek 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.

Rechecked current 2bf06cc7. error:null now takes the raw-body fallback before unmarshalling, and the same path covers string/non-object and non-JSON bodies while preserving *openai.Error, status, response, and raw body. The structured object case still parses normally. This resolves my earlier concern.

@andreynering andreynering changed the title fix: return API error instead of decode failure for non-object error bodies fix: return api error instead of decode failure for non-object error bodies Sep 12, 2026
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.

2 participants