Skip to content

Commit 6d46d54

Browse files
author
Nick Franck
committed
fix: only accept string failure_category values from raised errors
1 parent 2d15960 commit 6d46d54

2 files changed

Lines changed: 37 additions & 4 deletions

File tree

test/api/test_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
from pathlib import Path
23
from typing import Any, Optional, Union
34

@@ -619,3 +620,25 @@ def test_precheck_success_has_no_failure_category():
619620
body = resp.json()
620621
assert body["status_code"] == 200
621622
assert body["failure_category"] is None
623+
624+
625+
def test_precheck_ignores_non_string_failure_category():
626+
class _EnumCategoryFailure(Exception):
627+
status_code = 403
628+
failure_category = enum.Enum("Category", ["AUTH_PERMISSION_DENIED"]).AUTH_PERMISSION_DENIED
629+
630+
def _enum_category_precheck() -> None:
631+
raise _EnumCategoryFailure("credential rejected")
632+
633+
client = TestClient(
634+
wrap_in_fastapi(
635+
func=_no_params, plugin_id="mock_plugin", precheck_func=_enum_category_precheck
636+
)
637+
)
638+
639+
resp = client.get("/precheck")
640+
641+
body = resp.json()
642+
assert body["status_code"] == 403
643+
assert body["failure_category"] is None
644+
assert "credential rejected" in body["status_code_text"]

unstructured_platform_plugins/etl_uvicorn/api_generator.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ def log_func_and_body(func: Callable, body: Optional[str] = None) -> None:
6262
logger.log(level=logger.level, msg=msg)
6363

6464

65+
def failure_category_of(error: BaseException) -> Optional[str]:
66+
"""Return the error's failure_category only when it is a plain string.
67+
68+
Any other value would fail response-model validation inside an exception
69+
handler, replacing the sanitized error body with a raw 500.
70+
"""
71+
category = getattr(error, "failure_category", None)
72+
return category if isinstance(category, str) else None
73+
74+
6575
async def invoke_func(func: Callable, kwargs: Optional[dict[str, Any]] = None) -> Any:
6676
kwargs = kwargs or {}
6777
if inspect.iscoroutinefunction(func):
@@ -201,7 +211,7 @@ async def _stream_response():
201211
status_code=getattr(e, "status_code", None)
202212
or status.HTTP_500_INTERNAL_SERVER_ERROR,
203213
status_code_text=f"[{e.__class__.__name__}] {e}",
204-
failure_category=getattr(e, "failure_category", None),
214+
failure_category=failure_category_of(e),
205215
).model_dump_json()
206216
+ "\n"
207217
)
@@ -229,7 +239,7 @@ async def _stream_response():
229239
status_code_text=json.dumps(exc.detail)
230240
if isinstance(exc.detail, dict)
231241
else exc.detail,
232-
failure_category=getattr(exc, "failure_category", None),
242+
failure_category=failure_category_of(exc),
233243
file_data=request_dict.get("file_data", None),
234244
)
235245
except UnstructuredIngestError as exc:
@@ -243,7 +253,7 @@ async def _stream_response():
243253
filedata_meta=filedata_meta_model.model_validate(filedata_meta.model_dump()),
244254
status_code=exc.status_code or status.HTTP_500_INTERNAL_SERVER_ERROR,
245255
status_code_text=str(exc),
246-
failure_category=getattr(exc, "failure_category", None),
256+
failure_category=failure_category_of(exc),
247257
file_data=request_dict.get("file_data", None),
248258
)
249259
except Exception as invoke_error:
@@ -255,7 +265,7 @@ async def _stream_response():
255265
status_code=getattr(invoke_error, "status_code", None)
256266
or status.HTTP_500_INTERNAL_SERVER_ERROR,
257267
status_code_text=f"[{invoke_error.__class__.__name__}] {invoke_error}",
258-
failure_category=getattr(invoke_error, "failure_category", None),
268+
failure_category=failure_category_of(invoke_error),
259269
file_data=request_dict.get("file_data", None),
260270
)
261271

0 commit comments

Comments
 (0)