You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
APIStatusError.code is typed Optional[str] but can be an int at runtime #3531
construct_type returns the value unchanged when it doesn't match the target type — it neither coerces nor rejects — and the cast(Any, ...) suppresses the type checker, so nothing ever enforces the Optional[str] annotation. When an error body contains a JSON number ({"error": {"code": 404, ...}}), exc.code is an int.
This bit us in production: downstream code that trusted the annotation called exc.code.strip() and crashed with AttributeError: 'int' object has no attribute 'strip'. We observed the integer-code error body coming from api.openai.com itself (a one-off during a burst of concurrent chat completions), and OpenAI-compatible gateways/servers return integer codes routinely.
Suggested fix: either coerce non-None values to str when constructing the exception, or widen the annotation so type checkers tell consumers the truth.
To Reproduce
The value flows through construct_type unvalidated:
If APIStatusError.code is annotated Optional[str] but can be an int (or other JSON scalar) at runtime, any code that does code.startswith(...) / strict str matching will throw after the HTTP failure already happened — turning a classified API error into an unhandled local exception.
Defensive pattern until the type is widened/normalized:
raw=err.codecode=str(raw) ifrawisnotNoneelseNone# branch on code as str; never assume .startswith exists on the raw field
Also worth asserting in tests with a synthetic body {"error": {"code": 429, "message": "..."}} so the constructor path cannot regress to "typed str, runtime int" without CI noise.
This is the same class of bug as "expected vs actual" on error envelopes: the wire shape is the source of truth; the annotation should not invent a narrower type than the server emits.
Thanks for reporting this and explaining how it broke your error handling. The SDK’s loose deserializer could leave .code as an integer despite its str | None annotation. #3532 is now merged: non-null codes become strings, while .body retains the original value. This issue is closed because the SDK fix is on main.
The fix is not yet in the latest release, v3.13.0. Until a release includes it, use code = str(exc.code) if exc.code is not None else None before string operations. After upgrading to a release with the fix, change numeric comparisons such as exc.code == 404 to exc.code == "404", or read the original value from exc.body if needed.
Activity
IgorGanapolsky commentedon Jul 24, 2026
Type-contract reliability note (fail-closed consumers):
If
APIStatusError.codeis annotatedOptional[str]but can be anint(or other JSON scalar) at runtime, any code that doescode.startswith(...)/ strict str matching will throw after the HTTP failure already happened — turning a classified API error into an unhandled local exception.Defensive pattern until the type is widened/normalized:
Also worth asserting in tests with a synthetic body
{"error": {"code": 429, "message": "..."}}so the constructor path cannot regress to "typed str, runtime int" without CI noise.This is the same class of bug as "expected vs actual" on error envelopes: the wire shape is the source of truth; the annotation should not invent a narrower type than the server emits.
nightcityblade commentedon Jul 26, 2026
Hi, I'd like to work on this. I'll submit a PR shortly.
Fix APIStatusError.code type to accept int values
fix: type APIStatusError.code as int | str | None (openaigh-3531)
fix: normalize API error codes to strings (openai#3532)
marcuswood-oai commentedon Sep 14, 2026
Thanks for reporting this and explaining how it broke your error handling. The SDK’s loose deserializer could leave
.codeas an integer despite itsstr | Noneannotation. #3532 is now merged: non-null codes become strings, while.bodyretains the original value. This issue is closed because the SDK fix is on main.The fix is not yet in the latest release, v3.13.0. Until a release includes it, use
code = str(exc.code) if exc.code is not None else Nonebefore string operations. After upgrading to a release with the fix, change numeric comparisons such asexc.code == 404toexc.code == "404", or read the original value fromexc.bodyif needed.release: 3.14.0 (#3850)
fix(exceptions): coerce APIError.code to str to match Optional[str] a…