mirror of
https://github.com/furyhawk/agent_delta.git
synced 2026-07-21 02:05:36 +00:00
109 lines
3.2 KiB
Python
109 lines
3.2 KiB
Python
"""Exception handlers for FastAPI application.
|
|
|
|
These handlers convert domain exceptions to proper HTTP responses.
|
|
WebSocket connections that raise an ``AppException`` before ``accept()`` are
|
|
handled too — Starlette closes the socket with 403 and we just log the
|
|
incident; we cannot return an HTTP body for a non-HTTP scope.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.requests import HTTPConnection
|
|
|
|
from app.core.exceptions import AppException
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _connection_meta(conn: HTTPConnection) -> dict[str, Any]:
|
|
"""Common log fields shared by HTTP requests and WebSocket connections.
|
|
|
|
``method`` exists only on HTTP ``Request`` — for WebSockets we surface the
|
|
scope type so log filters can still distinguish the two.
|
|
"""
|
|
return {
|
|
"path": conn.url.path,
|
|
"method": getattr(conn, "method", None) or conn.scope.get("type", "unknown"),
|
|
}
|
|
|
|
|
|
def _is_websocket(conn: HTTPConnection) -> bool:
|
|
return conn.scope.get("type") == "websocket"
|
|
|
|
|
|
async def app_exception_handler(request: HTTPConnection, exc: AppException) -> JSONResponse | None:
|
|
"""Handle application exceptions for both HTTP and WebSocket scopes.
|
|
|
|
Logs 5xx errors as errors and 4xx as warnings. Returns a JSON response
|
|
for HTTP scopes; returns ``None`` for WebSocket scopes (Starlette will
|
|
close the socket on its own).
|
|
"""
|
|
log_extra = {
|
|
"error_code": exc.code,
|
|
"status_code": exc.status_code,
|
|
"details": exc.details,
|
|
**_connection_meta(request),
|
|
}
|
|
|
|
if exc.status_code >= 500:
|
|
logger.error(f"{exc.code}: {exc.message}", extra=log_extra)
|
|
else:
|
|
logger.warning(f"{exc.code}: {exc.message}", extra=log_extra)
|
|
|
|
if _is_websocket(request):
|
|
return None
|
|
|
|
headers: dict[str, str] = {}
|
|
if exc.status_code == 401:
|
|
headers["WWW-Authenticate"] = "Bearer"
|
|
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"error": {
|
|
"code": exc.code,
|
|
"message": exc.message,
|
|
"details": exc.details or None,
|
|
}
|
|
},
|
|
headers=headers,
|
|
)
|
|
|
|
|
|
async def unhandled_exception_handler(
|
|
request: HTTPConnection, exc: Exception
|
|
) -> JSONResponse | None:
|
|
"""Handle unexpected exceptions.
|
|
|
|
Logs the full exception but returns a generic error to the client
|
|
to avoid leaking sensitive information.
|
|
"""
|
|
logger.exception("Unhandled exception", extra=_connection_meta(request))
|
|
|
|
if _is_websocket(request):
|
|
return None
|
|
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": {
|
|
"code": "INTERNAL_ERROR",
|
|
"message": "An unexpected error occurred",
|
|
"details": None,
|
|
}
|
|
},
|
|
)
|
|
|
|
|
|
def register_exception_handlers(app: FastAPI) -> None:
|
|
"""Register all exception handlers on the FastAPI app.
|
|
|
|
Call this after creating the FastAPI application instance.
|
|
"""
|
|
app.add_exception_handler(AppException, app_exception_handler)
|
|
# Uncomment to catch all unhandled exceptions:
|
|
# app.add_exception_handler(Exception, unhandled_exception_handler)
|