From 8e72b7e0bcaac1065bbd551abf1aca6127c1db21 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 12 Feb 2026 20:10:30 +0530 Subject: [PATCH] feat(error_handling): improve error payload serialization for JSON compatibility --- backend/app/core/error_handling.py | 17 ++++++++++++++++- backend/tests/test_error_handling.py | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/backend/app/core/error_handling.py b/backend/app/core/error_handling.py index e2c0890f..24309cc8 100644 --- a/backend/app/core/error_handling.py +++ b/backend/app/core/error_handling.py @@ -224,12 +224,27 @@ def _get_request_id(request: Request) -> str | None: def _error_payload(*, detail: object, request_id: str | None) -> dict[str, object]: - payload: dict[str, Any] = {"detail": detail} + payload: dict[str, Any] = {"detail": _json_safe(detail)} if request_id: payload["request_id"] = request_id return payload +def _json_safe(value: object) -> object: + """Return a JSON-serializable representation for error payloads.""" + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + if isinstance(value, (bytearray, memoryview)): + return bytes(value).decode("utf-8", errors="replace") + if isinstance(value, dict): + return {str(key): _json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple, set)): + return [_json_safe(item) for item in value] + if value is None or isinstance(value, (str, int, float, bool)): + return value + return str(value) + + async def _request_validation_handler( request: Request, exc: RequestValidationError, diff --git a/backend/tests/test_error_handling.py b/backend/tests/test_error_handling.py index 3f922101..d8f1f5ac 100644 --- a/backend/tests/test_error_handling.py +++ b/backend/tests/test_error_handling.py @@ -38,6 +38,31 @@ def test_request_validation_error_includes_request_id(): assert resp.headers.get(REQUEST_ID_HEADER) == body["request_id"] +def test_request_validation_error_handles_bytes_input_without_500(): + class Payload(BaseModel): + content: str + + app = FastAPI() + install_error_handling(app) + + @app.put("/needs-object") + def needs_object(payload: Payload) -> dict[str, str]: + return {"content": payload.content} + + client = TestClient(app, raise_server_exceptions=False) + resp = client.put( + "/needs-object", + content=b"plain-text-body", + headers={"content-type": "text/plain"}, + ) + + assert resp.status_code == 422 + body = resp.json() + assert isinstance(body.get("detail"), list) + assert isinstance(body.get("request_id"), str) and body["request_id"] + assert resp.headers.get(REQUEST_ID_HEADER) == body["request_id"] + + def test_http_exception_includes_request_id(): app = FastAPI() install_error_handling(app)