Vault.MCP / backend /src /api /middleware /error_handlers.py
Wothmag07's picture
Updated UI changes with the backend functionality
b9a4f82
"""FastAPI exception handlers aligned with HTTP API contract."""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional, Tuple
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
logger = logging.getLogger(__name__)
DEFAULT_ERRORS: Dict[int, Tuple[str, str]] = {
status.HTTP_400_BAD_REQUEST: ("validation_error", "Invalid request payload"),
status.HTTP_401_UNAUTHORIZED: ("unauthorized", "Authorization required"),
status.HTTP_403_FORBIDDEN: ("forbidden", "Forbidden"),
status.HTTP_404_NOT_FOUND: ("not_found", "Resource not found"),
status.HTTP_409_CONFLICT: ("version_conflict", "Resource version conflict"),
status.HTTP_413_CONTENT_TOO_LARGE: (
"payload_too_large",
"Payload exceeds allowed size",
),
status.HTTP_500_INTERNAL_SERVER_ERROR: ("internal_error", "Internal server error"),
}
def _normalize_error(
status_code: int, detail: Any
) -> Tuple[str, str, Optional[Dict[str, Any]]]:
default_error, default_message = DEFAULT_ERRORS.get(
status_code, DEFAULT_ERRORS[status.HTTP_500_INTERNAL_SERVER_ERROR]
)
if isinstance(detail, dict):
error = detail.get("error", default_error)
message = detail.get("message", default_message)
detail_payload = detail.get("detail")
if detail_payload is None:
remainder = {
k: v for k, v in detail.items() if k not in {"error", "message", "detail"}
}
detail_payload = remainder or None
return error, message, detail_payload
if isinstance(detail, str) and detail:
return default_error, detail, None
return default_error, default_message, None
def _response(status_code: int, detail: Any) -> JSONResponse:
error, message, extra = _normalize_error(status_code, detail)
return JSONResponse(
status_code=status_code, content={"error": error, "message": message, "detail": extra}
)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
# Transform pydantic errors into more user-friendly format
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
errors.append({
"field": field or "request",
"reason": error["msg"],
"type": error["type"]
})
detail = {
"error": "validation_error",
"message": "Request validation failed",
"detail": {"fields": errors}
}
logger.warning(
"Validation error",
extra={"url": str(request.url), "errors": errors}
)
return _response(status.HTTP_400_BAD_REQUEST, detail)
async def http_exception_handler(
request: Request, exc: StarletteHTTPException
) -> JSONResponse:
logger.warning(
"HTTP exception",
extra={
"url": str(request.url),
"status_code": exc.status_code,
"detail": exc.detail
}
)
return _response(exc.status_code, exc.detail)
async def internal_exception_handler(request: Request, exc: Exception) -> JSONResponse:
logger.exception("Unhandled exception: %s", exc)
return _response(status.HTTP_500_INTERNAL_SERVER_ERROR, exc.args[0] if exc.args else None)
def register_error_handlers(app: FastAPI) -> None:
"""Attach shared exception handlers to the FastAPI application."""
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
app.add_exception_handler(Exception, internal_exception_handler)
__all__ = [
"register_error_handlers",
"validation_exception_handler",
"http_exception_handler",
"internal_exception_handler",
]