Manim-Agent / backend /core /errors.py
github-actions[bot]
[API] Cuong2004/Manim-Agent @ 1d7c417 (run 25583057312)
9bed109
from __future__ import annotations
import logging
from http import HTTPStatus
from typing import Any
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
from backend.core.config import settings
from backend.core.correlation import get_request_id
logger = logging.getLogger(__name__)
class AppException(Exception):
def __init__(
self,
*,
code: str,
message: str,
status_code: int = status.HTTP_400_BAD_REQUEST,
details: Any | None = None,
) -> None:
self.code = code
self.message = message
self.status_code = status_code
self.details = details
super().__init__(message)
class ResourceNotFound(AppException):
def __init__(self, resource: str, identifier: Any):
super().__init__(
code="not_found",
message=f"{resource} not found: {identifier}",
status_code=status.HTTP_404_NOT_FOUND,
)
# RFC 9110 name; Python 3.13+ exposes `UNPROCESSABLE_CONTENT` (422).
_VALIDATION_STATUS = getattr(HTTPStatus, "UNPROCESSABLE_CONTENT", HTTPStatus.UNPROCESSABLE_ENTITY)
def _error_payload(
*,
code: str,
message: str,
request_id: str | None,
details: Any | None = None,
) -> dict[str, Any]:
body: dict[str, Any] = {
"error": {
"code": code,
"message": message,
"request_id": request_id,
},
}
if details is not None:
body["details"] = jsonable_encoder(details)
return body
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
rid = get_request_id()
return JSONResponse(
status_code=exc.status_code,
content=_error_payload(
code=exc.code,
message=exc.message,
request_id=rid,
details=exc.details,
),
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(
request: Request,
exc: StarletteHTTPException,
) -> JSONResponse:
rid = get_request_id()
detail = exc.detail
if isinstance(detail, str):
message = detail
elif isinstance(detail, list):
message = "Request failed"
else:
message = str(detail)
return JSONResponse(
status_code=exc.status_code,
content=_error_payload(
code="http_error",
message=message,
request_id=rid,
),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
rid = get_request_id()
return JSONResponse(
status_code=_VALIDATION_STATUS,
content=_error_payload(
code="validation_error",
message="Request validation failed",
request_id=rid,
details=exc.errors(),
),
)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
rid = get_request_id()
logger.exception("Unhandled error (request_id=%s)", rid)
if settings.app_env == "production":
message = "Internal server error"
else:
message = str(exc)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=_error_payload(
code="internal_error",
message=message,
request_id=rid,
),
)