cbd0adca91
Stateful FastAPI lab with contract packs, Compose/Helm, Docker Hub release targets, and Pulumi coverage across all Engine series (GET/POST/PUT/DELETE/HEAD).
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
"""Base external error representation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ApiError(Exception):
|
|
"""A safe error intended for the Engine API boundary."""
|
|
|
|
def __init__(
|
|
self, status_code: int, message: str, errors: dict[str, str] | None = None
|
|
) -> None:
|
|
super().__init__(message)
|
|
self.status_code = status_code
|
|
self.message = message
|
|
self.errors = errors
|
|
|
|
|
|
class ContractValidationError(ApiError):
|
|
def __init__(self, errors: dict[str, str]) -> None:
|
|
super().__init__(400, "parameter verification failed", errors)
|
|
|
|
|
|
async def api_error_handler(_request: Request, exc: Exception) -> JSONResponse:
|
|
if not isinstance(exc, ApiError):
|
|
raise TypeError("api_error_handler received an incompatible exception")
|
|
body: dict[str, Any] = {"data": None, "message": exc.message}
|
|
if exc.errors is not None:
|
|
body["errors"] = exc.errors
|
|
return JSONResponse(status_code=exc.status_code, content=body)
|
|
|
|
|
|
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
"""Log internal failures and return a stable non-FastAPI error envelope."""
|
|
|
|
logger.exception(
|
|
"unhandled request error",
|
|
extra={"request_id": getattr(request.state, "request_id", None), "path": request.url.path},
|
|
)
|
|
body: dict[str, Any] = {
|
|
"data": None,
|
|
"errors": {"internal": "internal server error"},
|
|
}
|
|
return JSONResponse(status_code=500, content=body)
|