feat: expose the API's declared error verdict as typed attributes
What
HTTP errors from the client now carry the verdict the API declared, as typed attributes. Callers no longer have to re-parse err.body.
# Response: 503 {"detail": "OCR timed out", "retryable": false,
# "error_type": "timeout", "error_reason": "ocr_timeout", "audience": "platform"}
# Before
err.retryable # AttributeError
json.loads(err.body).get("retryable") # False, but every caller re-implements the parsing
# After
err.retryable # False
err.error_type # "timeout"
err.error_reason # "ocr_timeout"
err.audience # "platform"
| Error body | retryable / error_type / error_reason / audience |
|---|---|
| Declares all four fields | the declared values |
{"detail": "Internal server error"} (declares nothing) | all None |
A field with the wrong type, e.g. "retryable": "false" | that field is None |
| Not JSON, or not a JSON object | all None |
The attributes are on UnstructuredClientError, so they work for every HTTP error class at any status (SDKError for 4xx, ServerError, HTTPValidationError). That includes the response a failed split-PDF chunk returns.
Why
The API publishes retryable, error_type, error_reason and audience next to detail on errors it raises deliberately. The client's error models only expose detail, so any caller that wants to act on the verdict has to parse the raw body itself. Each caller then re-implements the same type checks and tends to get the edge cases slightly different.
The retry loop already reads retryable to stop retrying a declared non-retryable error (0.46.4). The loop and the error attributes now share one parser, so they cannot disagree about what a body declares. Both decode the response bytes, so a UTF-8 BOM or a UTF-16 body is read the same way response.json() reads it.
Notes for consumers
- Additive only. No existing attribute, message or retry decision changes.
- A
ResponseValidationErrorraised on a 2xx response always readsNone, and its (possibly large) body is not parsed again. - An unread streamed response is never consumed to read the verdict.
Validation
- Unit suite: 403 passed, 1 xfailed.
make lintis clean (pylint 10/10, mypy). - New tests cover the parser's type rules, each error class with UTF-8, UTF-8-BOM and UTF-16 bodies, the unread-stream fallback, 2xx validation errors, and public
partition/partition_async(400 and 503) through a mocked transport. - Four rounds of Codex review. Rounds 2 and 3 found that decoding text instead of bytes lost JSON encoding detection, in the retry check and then in the error attributes. Both are fixed with tests.