feat(error_handling): improve error payload serialization for JSON compatibility
This commit is contained in:
@@ -224,12 +224,27 @@ def _get_request_id(request: Request) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def _error_payload(*, detail: object, request_id: str | None) -> dict[str, object]:
|
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:
|
if request_id:
|
||||||
payload["request_id"] = request_id
|
payload["request_id"] = request_id
|
||||||
return payload
|
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(
|
async def _request_validation_handler(
|
||||||
request: Request,
|
request: Request,
|
||||||
exc: RequestValidationError,
|
exc: RequestValidationError,
|
||||||
|
|||||||
@@ -38,6 +38,31 @@ def test_request_validation_error_includes_request_id():
|
|||||||
assert resp.headers.get(REQUEST_ID_HEADER) == body["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():
|
def test_http_exception_includes_request_id():
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
install_error_handling(app)
|
install_error_handling(app)
|
||||||
|
|||||||
Reference in New Issue
Block a user