From 89d9f9b4b069d3814ba009689634c04440442674 Mon Sep 17 00:00:00 2001 From: Nick Franck <46548427+CyMule@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:30:41 -0400 Subject: [PATCH 1/4] feat: expose the API's declared error verdict as typed attributes --- CHANGELOG.md | 5 + README.md | 4 + .../unit/test_declared_errors.py | 139 ++++++++++++++++++ src/unstructured_client/_version.py | 4 +- .../models/errors/unstructuredclienterror.py | 12 ++ .../utils/declared_errors.py | 44 ++++++ src/unstructured_client/utils/retries.py | 4 +- 7 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 _test_unstructured_client/unit/test_declared_errors.py create mode 100644 src/unstructured_client/utils/declared_errors.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e2c031..0a033100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.47.0 + +### Features +* Expose the verdict the API declares on an error as typed attributes on every HTTP error: `retryable`, `error_type`, `error_reason` and `audience`. Errors the API raises deliberately publish these fields next to `detail`, eg a 503 OCR timeout with `"retryable": false`. Callers no longer need to parse `err.body` to read them. Each attribute is `None` when the body is not JSON, the field is absent, or its value has the wrong type. The retry loop reads `retryable` with the same parser. + ## 0.46.4 ### Fixes diff --git a/README.md b/README.md index 897e659e..ed038cef 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,10 @@ with UnstructuredClient( | `err.body` | `str` | HTTP body. Can be empty string if no body is returned. | | `err.raw_response` | `httpx.Response` | Raw HTTP response | | `err.data` | | Optional. Some errors may contain structured data. [See Error Classes](#error-classes). | +| `err.retryable` | `Optional[bool]` | Whether the API declared that sending the same request again can succeed. `None` if it declared nothing. | +| `err.error_type` | `Optional[str]` | The kind of failure the API declared, eg `timeout`. `None` if it declared nothing. | +| `err.error_reason` | `Optional[str]` | A stable name for the failure the API declared. `None` if it declared nothing. | +| `err.audience` | `Optional[str]` | Whose fault the API declared the failure to be, `user` or `platform`. `None` if it declared nothing. | ### Example ```python diff --git a/_test_unstructured_client/unit/test_declared_errors.py b/_test_unstructured_client/unit/test_declared_errors.py new file mode 100644 index 00000000..f244232f --- /dev/null +++ b/_test_unstructured_client/unit/test_declared_errors.py @@ -0,0 +1,139 @@ +import asyncio + +import httpx +import pytest + +from unstructured_client import UnstructuredClient +from unstructured_client.models.errors import ( + HTTPValidationError, + HTTPValidationErrorData, + SDKError, + ServerError, + ServerErrorData, +) +from unstructured_client.utils import BackoffStrategy, RetryConfig +from unstructured_client.utils.declared_errors import ( + DeclaredErrorFields, + parse_declared_error_fields, +) + +DECLARED_BODY = { + "detail": "OCR timed out", + "retryable": False, + "error_type": "timeout", + "error_reason": "ocr_timeout", + "audience": "platform", +} +DECLARED_FIELDS = DeclaredErrorFields( + retryable=False, error_type="timeout", error_reason="ocr_timeout", audience="platform" +) + + +def _response(status_code: int, **kwargs) -> httpx.Response: + return httpx.Response( + status_code, + request=httpx.Request("POST", "http://test.invalid/general/v0/general"), + **kwargs, + ) + + +def _declared(error) -> DeclaredErrorFields: + return DeclaredErrorFields( + retryable=error.retryable, + error_type=error.error_type, + error_reason=error.error_reason, + audience=error.audience, + ) + + +@pytest.mark.parametrize( + "text,expected", + [ + ('{"detail":"x","retryable":false,"error_type":"timeout",' + '"error_reason":"ocr_timeout","audience":"platform"}', DECLARED_FIELDS), + ('{"retryable":true}', DeclaredErrorFields(retryable=True)), + ('{"detail":"x","retryable":false,"audience":null}', DeclaredErrorFields(retryable=False)), + ('{"detail":"Internal server error"}', DeclaredErrorFields()), + ('{"retryable":"false","error_type":5,"error_reason":" ","audience":[]}', + DeclaredErrorFields()), + ('{"retryable":0}', DeclaredErrorFields()), + ('[{"retryable":false}]', DeclaredErrorFields()), + ("false", DeclaredErrorFields()), + ("{broken", DeclaredErrorFields()), + ("upstream error", DeclaredErrorFields()), + ("", DeclaredErrorFields()), + (None, DeclaredErrorFields()), + pytest.param( + '{"retryable":false,"detail":' + "[" * 100_000 + "]" * 100_000 + "}", + DeclaredErrorFields(), + id="deeply-nested", + ), + ], +) +def test_parse_declared_error_fields(text, expected): + assert parse_declared_error_fields(text) == expected + + +def test_server_error_exposes_declared_fields(): + response = _response(503, json=DECLARED_BODY) + error = ServerError(ServerErrorData(detail="OCR timed out"), response) + + assert _declared(error) == DECLARED_FIELDS + assert error.data.detail == "OCR timed out" + + +def test_sdk_error_exposes_declared_fields(): + response = _response(400, json=DECLARED_BODY) + error = SDKError("API error occurred", response, response.text) + + assert _declared(error) == DECLARED_FIELDS + + +def test_http_validation_error_exposes_declared_fields(): + response = _response(422, json=DECLARED_BODY) + error = HTTPValidationError(HTTPValidationErrorData(detail="OCR timed out"), response) + + assert _declared(error) == DECLARED_FIELDS + + +def test_explicit_body_takes_precedence_over_response_text(): + response = _response(500, json={"detail": "x"}) + error = SDKError("API error occurred", response, '{"retryable":false}') + + assert error.retryable is False + + +def test_undeclared_error_reads_none(): + response = _response(500, json={"detail": "Internal server error"}) + error = ServerError(ServerErrorData(detail="Internal server error"), response) + + assert _declared(error) == DeclaredErrorFields() + + +@pytest.mark.parametrize("async_mode", [False, True]) +@pytest.mark.parametrize("status_code,error_class", [(400, SDKError), (503, ServerError)]) +def test_public_partition_raises_declared_fields(async_mode, status_code, error_class): + def respond(request): + return httpx.Response(status_code, json=DECLARED_BODY) + + async def run(): + transport = httpx.MockTransport(respond) + with httpx.Client(transport=transport) as client: + async with httpx.AsyncClient(transport=transport) as async_client: + sdk = UnstructuredClient( + server_url="http://test.invalid", client=client, async_client=async_client + ) + request = {"partition_parameters": { + "files": {"content": b"hello", "file_name": "test.txt"}, + "split_pdf_page": False, + }} + retries = RetryConfig("none", BackoffStrategy(1, 1, 1.0, 1), False) + with pytest.raises(error_class) as exc: + if async_mode: + await sdk.general.partition_async(request=request, retries=retries) + else: + sdk.general.partition(request=request, retries=retries) + return exc.value + + error = asyncio.run(run()) + assert _declared(error) == DECLARED_FIELDS diff --git a/src/unstructured_client/_version.py b/src/unstructured_client/_version.py index 43ed3386..51a85b03 100644 --- a/src/unstructured_client/_version.py +++ b/src/unstructured_client/_version.py @@ -3,10 +3,10 @@ import importlib.metadata __title__: str = "unstructured-client" -__version__: str = "0.46.4" +__version__: str = "0.47.0" __openapi_doc_version__: str = "1.2.31" __gen_version__: str = "2.680.0" -__user_agent__: str = "speakeasy-sdk/python 0.46.4 2.680.0 1.2.31 unstructured-client" +__user_agent__: str = "speakeasy-sdk/python 0.47.0 2.680.0 1.2.31 unstructured-client" try: if __package__ is not None: diff --git a/src/unstructured_client/models/errors/unstructuredclienterror.py b/src/unstructured_client/models/errors/unstructuredclienterror.py index 054fb875..49c1be1c 100644 --- a/src/unstructured_client/models/errors/unstructuredclienterror.py +++ b/src/unstructured_client/models/errors/unstructuredclienterror.py @@ -3,6 +3,8 @@ import httpx from typing import Optional +from unstructured_client.utils.declared_errors import parse_declared_error_fields + class UnstructuredClientError(Exception): """The base class for all HTTP error responses.""" @@ -12,6 +14,10 @@ class UnstructuredClientError(Exception): body: str headers: httpx.Headers raw_response: httpx.Response + retryable: Optional[bool] + error_type: Optional[str] + error_reason: Optional[str] + audience: Optional[str] def __init__( self, message: str, raw_response: httpx.Response, body: Optional[str] = None @@ -22,5 +28,11 @@ def __init__( self.headers = raw_response.headers self.raw_response = raw_response + declared = parse_declared_error_fields(self.body) + self.retryable = declared.retryable + self.error_type = declared.error_type + self.error_reason = declared.error_reason + self.audience = declared.audience + def __str__(self): return self.message diff --git a/src/unstructured_client/utils/declared_errors.py b/src/unstructured_client/utils/declared_errors.py new file mode 100644 index 00000000..c7a60a8a --- /dev/null +++ b/src/unstructured_client/utils/declared_errors.py @@ -0,0 +1,44 @@ +"""The verdict the Unstructured API declares beside ``detail`` in an error body. + +Errors the API raises deliberately carry ``retryable``, ``error_type``, +``error_reason`` and ``audience`` next to ``detail``. Errors it did not classify +carry only ``detail``, and every field reads as ``None``. +""" + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class DeclaredErrorFields: + retryable: Optional[bool] = None + error_type: Optional[str] = None + error_reason: Optional[str] = None + audience: Optional[str] = None + + +def _non_empty_str(value: Any) -> Optional[str]: + return value if isinstance(value, str) and value.strip() else None + + +def declared_error_fields(body: Any) -> DeclaredErrorFields: + """Read the declared fields from a decoded body, dropping any of the wrong type.""" + if not isinstance(body, dict): + return DeclaredErrorFields() + retryable = body.get("retryable") + return DeclaredErrorFields( + retryable=retryable if isinstance(retryable, bool) else None, + error_type=_non_empty_str(body.get("error_type")), + error_reason=_non_empty_str(body.get("error_reason")), + audience=_non_empty_str(body.get("audience")), + ) + + +def parse_declared_error_fields(text: Optional[str]) -> DeclaredErrorFields: + """Read the declared fields from a raw body; a non-JSON body declares nothing.""" + try: + body = json.loads(text) if text else None + except (ValueError, TypeError, RecursionError): + return DeclaredErrorFields() + return declared_error_fields(body) diff --git a/src/unstructured_client/utils/retries.py b/src/unstructured_client/utils/retries.py index d268fefb..94492aa7 100644 --- a/src/unstructured_client/utils/retries.py +++ b/src/unstructured_client/utils/retries.py @@ -7,6 +7,8 @@ import httpx +from unstructured_client.utils.declared_errors import declared_error_fields + class BackoffStrategy: initial_interval: int @@ -93,7 +95,7 @@ def _declares_non_retryable(response: httpx.Response) -> bool: body = response.json() except (ValueError, RecursionError, httpx.ResponseNotRead): return False - return isinstance(body, dict) and body.get("retryable") is False + return declared_error_fields(body).retryable is False def _is_retry_status(response: httpx.Response, status_codes: List[str]) -> bool: From b72b356172ca46cd3a39c61c397e307b32b9d348 Mon Sep 17 00:00:00 2001 From: Nick Franck <46548427+CyMule@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:34:22 -0400 Subject: [PATCH 2/4] refactor: share one decode path and skip parsing success bodies --- .../unit/test_declared_errors.py | 80 +++++++++---------- .../models/errors/unstructuredclienterror.py | 13 ++- .../utils/declared_errors.py | 10 ++- src/unstructured_client/utils/retries.py | 8 +- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/_test_unstructured_client/unit/test_declared_errors.py b/_test_unstructured_client/unit/test_declared_errors.py index f244232f..18adcba6 100644 --- a/_test_unstructured_client/unit/test_declared_errors.py +++ b/_test_unstructured_client/unit/test_declared_errors.py @@ -7,6 +7,7 @@ from unstructured_client.models.errors import ( HTTPValidationError, HTTPValidationErrorData, + ResponseValidationError, SDKError, ServerError, ServerErrorData, @@ -74,26 +75,17 @@ def test_parse_declared_error_fields(text, expected): assert parse_declared_error_fields(text) == expected -def test_server_error_exposes_declared_fields(): - response = _response(503, json=DECLARED_BODY) - error = ServerError(ServerErrorData(detail="OCR timed out"), response) - - assert _declared(error) == DECLARED_FIELDS - assert error.data.detail == "OCR timed out" - - -def test_sdk_error_exposes_declared_fields(): - response = _response(400, json=DECLARED_BODY) - error = SDKError("API error occurred", response, response.text) - - assert _declared(error) == DECLARED_FIELDS - - -def test_http_validation_error_exposes_declared_fields(): - response = _response(422, json=DECLARED_BODY) - error = HTTPValidationError(HTTPValidationErrorData(detail="OCR timed out"), response) - - assert _declared(error) == DECLARED_FIELDS +@pytest.mark.parametrize( + "build", + [ + lambda r: ServerError(ServerErrorData(detail="OCR timed out"), r), + lambda r: SDKError("API error occurred", r, r.text), + lambda r: HTTPValidationError(HTTPValidationErrorData(detail="OCR timed out"), r), + ], + ids=["ServerError", "SDKError", "HTTPValidationError"], +) +def test_error_classes_expose_declared_fields(build): + assert _declared(build(_response(503, json=DECLARED_BODY))) == DECLARED_FIELDS def test_explicit_body_takes_precedence_over_response_text(): @@ -103,9 +95,9 @@ def test_explicit_body_takes_precedence_over_response_text(): assert error.retryable is False -def test_undeclared_error_reads_none(): - response = _response(500, json={"detail": "Internal server error"}) - error = ServerError(ServerErrorData(detail="Internal server error"), response) +def test_success_response_declares_nothing(): + response = _response(200, json=DECLARED_BODY) + error = ResponseValidationError("Response validation failed", response, ValueError()) assert _declared(error) == DeclaredErrorFields() @@ -116,24 +108,24 @@ def test_public_partition_raises_declared_fields(async_mode, status_code, error_ def respond(request): return httpx.Response(status_code, json=DECLARED_BODY) - async def run(): - transport = httpx.MockTransport(respond) - with httpx.Client(transport=transport) as client: - async with httpx.AsyncClient(transport=transport) as async_client: - sdk = UnstructuredClient( - server_url="http://test.invalid", client=client, async_client=async_client - ) - request = {"partition_parameters": { - "files": {"content": b"hello", "file_name": "test.txt"}, - "split_pdf_page": False, - }} - retries = RetryConfig("none", BackoffStrategy(1, 1, 1.0, 1), False) - with pytest.raises(error_class) as exc: - if async_mode: - await sdk.general.partition_async(request=request, retries=retries) - else: - sdk.general.partition(request=request, retries=retries) - return exc.value - - error = asyncio.run(run()) - assert _declared(error) == DECLARED_FIELDS + transport = httpx.MockTransport(respond) + request = {"partition_parameters": { + "files": {"content": b"hello", "file_name": "test.txt"}, + "split_pdf_page": False, + }} + retries = RetryConfig("none", BackoffStrategy(1, 1, 1.0, 1), False) + + async def run_async(): + async with httpx.AsyncClient(transport=transport) as async_client: + sdk = UnstructuredClient(server_url="http://test.invalid", async_client=async_client) + await sdk.general.partition_async(request=request, retries=retries) + + with pytest.raises(error_class) as exc: + if async_mode: + asyncio.run(run_async()) + else: + with httpx.Client(transport=transport) as client: + sdk = UnstructuredClient(server_url="http://test.invalid", client=client) + sdk.general.partition(request=request, retries=retries) + + assert _declared(exc.value) == DECLARED_FIELDS diff --git a/src/unstructured_client/models/errors/unstructuredclienterror.py b/src/unstructured_client/models/errors/unstructuredclienterror.py index 49c1be1c..a2a267d0 100644 --- a/src/unstructured_client/models/errors/unstructuredclienterror.py +++ b/src/unstructured_client/models/errors/unstructuredclienterror.py @@ -3,7 +3,10 @@ import httpx from typing import Optional -from unstructured_client.utils.declared_errors import parse_declared_error_fields +from unstructured_client.utils.declared_errors import ( + DeclaredErrorFields, + parse_declared_error_fields, +) class UnstructuredClientError(Exception): @@ -28,7 +31,13 @@ def __init__( self.headers = raw_response.headers self.raw_response = raw_response - declared = parse_declared_error_fields(self.body) + # A success response that failed validation can carry a large body that + # declares nothing, so only error statuses are parsed. + declared = ( + parse_declared_error_fields(self.body) + if self.status_code >= 400 + else DeclaredErrorFields() + ) self.retryable = declared.retryable self.error_type = declared.error_type self.error_reason = declared.error_reason diff --git a/src/unstructured_client/utils/declared_errors.py b/src/unstructured_client/utils/declared_errors.py index c7a60a8a..7cfee920 100644 --- a/src/unstructured_client/utils/declared_errors.py +++ b/src/unstructured_client/utils/declared_errors.py @@ -22,8 +22,7 @@ def _non_empty_str(value: Any) -> Optional[str]: return value if isinstance(value, str) and value.strip() else None -def declared_error_fields(body: Any) -> DeclaredErrorFields: - """Read the declared fields from a decoded body, dropping any of the wrong type.""" +def _declared_error_fields(body: Any) -> DeclaredErrorFields: if not isinstance(body, dict): return DeclaredErrorFields() retryable = body.get("retryable") @@ -36,9 +35,12 @@ def declared_error_fields(body: Any) -> DeclaredErrorFields: def parse_declared_error_fields(text: Optional[str]) -> DeclaredErrorFields: - """Read the declared fields from a raw body; a non-JSON body declares nothing.""" + """Read the declared fields from a raw body, dropping any of the wrong type. + + A body that is not a JSON object declares nothing. + """ try: body = json.loads(text) if text else None except (ValueError, TypeError, RecursionError): return DeclaredErrorFields() - return declared_error_fields(body) + return _declared_error_fields(body) diff --git a/src/unstructured_client/utils/retries.py b/src/unstructured_client/utils/retries.py index 94492aa7..bb4d6d16 100644 --- a/src/unstructured_client/utils/retries.py +++ b/src/unstructured_client/utils/retries.py @@ -7,7 +7,7 @@ import httpx -from unstructured_client.utils.declared_errors import declared_error_fields +from unstructured_client.utils.declared_errors import parse_declared_error_fields class BackoffStrategy: @@ -92,10 +92,10 @@ def __init__(self, inner: Exception): def _declares_non_retryable(response: httpx.Response) -> bool: # Inspect buffered error bodies without consuming unread response streams. try: - body = response.json() - except (ValueError, RecursionError, httpx.ResponseNotRead): + text = response.text + except httpx.ResponseNotRead: return False - return declared_error_fields(body).retryable is False + return parse_declared_error_fields(text).retryable is False def _is_retry_status(response: httpx.Response, status_codes: List[str]) -> bool: From 292090cbe68c868129fede3aac7833a8a729c596 Mon Sep 17 00:00:00 2001 From: Nick Franck <46548427+CyMule@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:36:01 -0400 Subject: [PATCH 3/4] fix: decode retry bodies as JSON bytes to keep encoding detection --- _test_unstructured_client/unit/test_retries.py | 2 ++ src/unstructured_client/utils/declared_errors.py | 9 +++++---- src/unstructured_client/utils/retries.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/_test_unstructured_client/unit/test_retries.py b/_test_unstructured_client/unit/test_retries.py index 6419b9d4..bce7175c 100644 --- a/_test_unstructured_client/unit/test_retries.py +++ b/_test_unstructured_client/unit/test_retries.py @@ -599,6 +599,8 @@ async def run(): (b'upstream error', "text/plain", False), (b'{"retryable":false}', "text/plain", True), (b'{"retryable":false}', "", True), + (b'\xef\xbb\xbf{"retryable":false}', "application/json", True), + ('{"retryable":false}'.encode("utf-16"), "application/json", True), ], ) def test_declared_retry_verdict( diff --git a/src/unstructured_client/utils/declared_errors.py b/src/unstructured_client/utils/declared_errors.py index 7cfee920..64af26fb 100644 --- a/src/unstructured_client/utils/declared_errors.py +++ b/src/unstructured_client/utils/declared_errors.py @@ -7,7 +7,7 @@ import json from dataclasses import dataclass -from typing import Any, Optional +from typing import Any, Optional, Union @dataclass(frozen=True) @@ -34,13 +34,14 @@ def _declared_error_fields(body: Any) -> DeclaredErrorFields: ) -def parse_declared_error_fields(text: Optional[str]) -> DeclaredErrorFields: +def parse_declared_error_fields(raw: Union[str, bytes, None]) -> DeclaredErrorFields: """Read the declared fields from a raw body, dropping any of the wrong type. - A body that is not a JSON object declares nothing. + Bytes are decoded as JSON, which detects UTF-8, UTF-16 and UTF-32 and skips a + UTF-8 byte order mark. A body that is not a JSON object declares nothing. """ try: - body = json.loads(text) if text else None + body = json.loads(raw) if raw else None except (ValueError, TypeError, RecursionError): return DeclaredErrorFields() return _declared_error_fields(body) diff --git a/src/unstructured_client/utils/retries.py b/src/unstructured_client/utils/retries.py index bb4d6d16..6d4e4f67 100644 --- a/src/unstructured_client/utils/retries.py +++ b/src/unstructured_client/utils/retries.py @@ -92,10 +92,10 @@ def __init__(self, inner: Exception): def _declares_non_retryable(response: httpx.Response) -> bool: # Inspect buffered error bodies without consuming unread response streams. try: - text = response.text + content = response.content except httpx.ResponseNotRead: return False - return parse_declared_error_fields(text).retryable is False + return parse_declared_error_fields(content).retryable is False def _is_retry_status(response: httpx.Response, status_codes: List[str]) -> bool: From 1132aaceefd74b11f1762aef4b39ac97243c7519 Mon Sep 17 00:00:00 2001 From: Nick Franck <46548427+CyMule@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:37:54 -0400 Subject: [PATCH 4/4] fix: read declared error fields from the response bytes --- .../unit/test_declared_errors.py | 29 +++++++++++++++---- .../models/errors/unstructuredclienterror.py | 11 +++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/_test_unstructured_client/unit/test_declared_errors.py b/_test_unstructured_client/unit/test_declared_errors.py index 18adcba6..e3cd2937 100644 --- a/_test_unstructured_client/unit/test_declared_errors.py +++ b/_test_unstructured_client/unit/test_declared_errors.py @@ -1,4 +1,5 @@ import asyncio +import json import httpx import pytest @@ -84,15 +85,31 @@ def test_parse_declared_error_fields(text, expected): ], ids=["ServerError", "SDKError", "HTTPValidationError"], ) -def test_error_classes_expose_declared_fields(build): - assert _declared(build(_response(503, json=DECLARED_BODY))) == DECLARED_FIELDS +@pytest.mark.parametrize( + "content", + [ + json.dumps(DECLARED_BODY).encode(), + b"\xef\xbb\xbf" + json.dumps(DECLARED_BODY).encode(), + json.dumps(DECLARED_BODY).encode("utf-16"), + ], + ids=["utf-8", "utf-8-bom", "utf-16"], +) +def test_error_classes_expose_declared_fields(build, content): + response = _response(503, content=content, headers={"Content-Type": "application/json"}) + + assert _declared(build(response)) == DECLARED_FIELDS + +def test_unread_stream_falls_back_to_body(): + class Stream(httpx.SyncByteStream): + def __iter__(self): + pytest.fail("error construction read an unread stream") + yield b"" -def test_explicit_body_takes_precedence_over_response_text(): - response = _response(500, json={"detail": "x"}) - error = SDKError("API error occurred", response, '{"retryable":false}') + response = _response(503, stream=Stream()) + error = SDKError("API error occurred", response, json.dumps(DECLARED_BODY)) - assert error.retryable is False + assert _declared(error) == DECLARED_FIELDS def test_success_response_declares_nothing(): diff --git a/src/unstructured_client/models/errors/unstructuredclienterror.py b/src/unstructured_client/models/errors/unstructuredclienterror.py index a2a267d0..ac8ee9c1 100644 --- a/src/unstructured_client/models/errors/unstructuredclienterror.py +++ b/src/unstructured_client/models/errors/unstructuredclienterror.py @@ -1,7 +1,7 @@ """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" import httpx -from typing import Optional +from typing import Optional, Union from unstructured_client.utils.declared_errors import ( DeclaredErrorFields, @@ -34,7 +34,7 @@ def __init__( # A success response that failed validation can carry a large body that # declares nothing, so only error statuses are parsed. declared = ( - parse_declared_error_fields(self.body) + parse_declared_error_fields(self._raw_body()) if self.status_code >= 400 else DeclaredErrorFields() ) @@ -43,5 +43,12 @@ def __init__( self.error_reason = declared.error_reason self.audience = declared.audience + def _raw_body(self) -> Union[str, bytes]: + # The response bytes keep the JSON encoding detection that decoded text loses. + try: + return self.raw_response.content + except httpx.ResponseNotRead: + return self.body + def __str__(self): return self.message