Skip to content

Commit 9ab9e32

Browse files
author
Nick Franck
committed
fix: close remaining error-contract gaps in the invoke handlers
Clamp status_code_of to the HTTP status range (0 regressed to being served verbatim instead of falling back to 500), read status_code through the guarded accessor in the UnstructuredIngestError log line, and resolve string/postponed annotations before validating precheck signatures.
1 parent a02a11c commit 9ab9e32

2 files changed

Lines changed: 57 additions & 7 deletions

File tree

test/api/test_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,46 @@ def _bad_precheck(usage: int) -> None:
719719

720720
with pytest.raises(EtlApiException):
721721
wrap_in_fastapi(func=_no_params, plugin_id="mock_plugin", precheck_func=_bad_precheck)
722+
723+
724+
def test_invoke_clamps_out_of_range_status_code():
725+
class _ZeroStatusError(Exception):
726+
status_code = 0
727+
728+
def _raising_func() -> None:
729+
raise _ZeroStatusError("boom")
730+
731+
client = TestClient(wrap_in_fastapi(func=_raising_func, plugin_id="mock_plugin"))
732+
733+
assert client.post("/invoke").json()["status_code"] == 500
734+
735+
736+
def test_invoke_survives_ingest_error_with_raising_status_code():
737+
from unstructured_ingest.error import UnstructuredIngestError
738+
739+
class _HostileIngestError(UnstructuredIngestError):
740+
@property
741+
def status_code(self) -> int:
742+
raise RuntimeError("status_code exploded")
743+
744+
def _raising_func() -> None:
745+
raise _HostileIngestError("boom")
746+
747+
client = TestClient(wrap_in_fastapi(func=_raising_func, plugin_id="mock_plugin"))
748+
749+
resp = client.post("/invoke")
750+
assert resp.status_code == 200
751+
assert resp.json()["status_code"] == 500
752+
753+
754+
def test_precheck_func_accepts_string_annotations():
755+
def _string_annotated_precheck(usage: "list") -> "None":
756+
return None
757+
758+
client = TestClient(
759+
wrap_in_fastapi(
760+
func=_no_params, plugin_id="mock_plugin", precheck_func=_string_annotated_precheck
761+
)
762+
)
763+
764+
assert client.get("/precheck").json()["status_code"] == 200

unstructured_platform_plugins/etl_uvicorn/api_generator.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,13 @@ def failure_category_of(error: BaseException) -> Optional[str]:
8282

8383

8484
def status_code_of(error: BaseException) -> int:
85-
"""Return the error's status_code only when it is a usable integer, else 500."""
85+
"""Return the error's status_code only when it is an int in the HTTP range, else 500."""
8686
status_code = _error_attr(error, "status_code")
87-
if isinstance(status_code, int) and not isinstance(status_code, bool):
87+
if (
88+
isinstance(status_code, int)
89+
and not isinstance(status_code, bool)
90+
and 100 <= status_code <= 599
91+
):
8892
return status_code
8993
return status.HTTP_500_INTERNAL_SERVER_ERROR
9094

@@ -98,7 +102,11 @@ async def invoke_func(func: Callable, kwargs: Optional[dict[str, Any]] = None) -
98102

99103

100104
def check_precheck_func(precheck_func: Callable):
101-
sig = inspect.signature(precheck_func)
105+
try:
106+
# eval_str resolves postponed/string annotations ('list', 'None')
107+
sig = inspect.signature(precheck_func, eval_str=True)
108+
except (NameError, TypeError):
109+
sig = inspect.signature(precheck_func)
102110
inputs = list(sig.parameters.values())
103111
outputs = sig.return_annotation
104112
if len(inputs) == 1:
@@ -265,7 +273,8 @@ async def _stream_response():
265273
)
266274
except UnstructuredIngestError as exc:
267275
logger.error(
268-
f"UnstructuredIngestError: {str(exc)} (status_code={exc.status_code})",
276+
f"UnstructuredIngestError: {exc} "
277+
f"(status_code={_error_attr(exc, 'status_code')})",
269278
exc_info=True,
270279
)
271280
return InvokeResponse(
@@ -322,9 +331,7 @@ async def run_job_with_body(request: BaseModel) -> ResponseType:
322331

323332
@fastapi_app.post("/invoke", response_model=InvokeResponse)
324333
async def run_job(request: Optional[input_schema_model] = None) -> ResponseType:
325-
return await run_job_with_body(
326-
request if request is not None else input_schema_model()
327-
)
334+
return await run_job_with_body(request if request is not None else input_schema_model())
328335

329336
elif input_schema_model.model_fields:
330337

0 commit comments

Comments
 (0)