Spaces:
Paused
Paused
File size: 2,724 Bytes
8c1b9fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | """Structured error handling for the API.
Every error response — AuralynqError, a plain FastAPI/Starlette
HTTPException (404s, the auth middleware's 401), a 422 request-validation
error, or an unhandled exception — uses the same envelope so a client only
ever parses one shape:
{"error": {"code": "...", "message": "...", "details": {...}, "trace_id": "..."}}
`trace_id` is the same per-request ID already exposed via the `X-Request-ID`
response header (see the request_id middleware in auralynq/serving/app.py).
"""
from __future__ import annotations
from typing import Any
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
def _trace_id(request: Request) -> str:
return getattr(request.state, "request_id", "")
def _envelope(code: str, message: str, details: dict[str, Any], trace_id: str) -> dict[str, Any]:
return {"error": {"code": code, "message": message, "details": details, "trace_id": trace_id}}
class AuralynqError(Exception):
"""Raise with a short machine-readable ``code`` and a human ``detail``."""
def __init__(
self,
code: str,
*,
status_code: int = 400,
detail: str = "",
details: dict[str, Any] | None = None,
) -> None:
super().__init__(code)
self.code = code
self.status_code = status_code
self.message = detail or code
self.details = details or {}
async def auralynq_error_handler(request: Request, exc: AuralynqError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content=_envelope(exc.code, exc.message, exc.details, _trace_id(request)),
)
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
"""Covers plain `raise HTTPException(...)` calls and framework 404s/405s."""
return JSONResponse(
status_code=exc.status_code,
content=_envelope("http_error", str(exc.detail), {}, _trace_id(request)),
headers=getattr(exc, "headers", None),
)
async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(
status_code=422,
content=_envelope(
"validation_error",
"Request validation failed",
{"errors": exc.errors()},
_trace_id(request),
),
)
async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
return JSONResponse(
status_code=500,
content=_envelope("internal_error", str(exc), {}, _trace_id(request)),
)
|