From 198d8113eaa104867c753cd314f21ac2a33afdb0 Mon Sep 17 00:00:00 2001 From: Alan Chauchet Date: Mon, 1 Jun 2026 10:31:21 +0200 Subject: [PATCH] feat(api): logs error details on HttpException --- src/main.py | 31 ++++++++++++++++++++++++++++-- src/tests/unit/test_logging.py | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/tests/unit/test_logging.py diff --git a/src/main.py b/src/main.py index 25db5a4..945e0a4 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,13 @@ import sentry_sdk from fastapi import FastAPI, HTTPException, Request +from fastapi.exception_handlers import http_exception_handler from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.logging import LoggingIntegration +from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.sessions import SessionMiddleware from src.config import settings @@ -62,18 +64,43 @@ ) +@app.exception_handler(StarletteHTTPException) +async def log_http_exception(request: Request, exc: StarletteHTTPException): + sentry_sdk.capture_exception(exc) + app_logger.error( + "HTTPException %s on %s %s: %s", + exc.status_code, + request.method, + request.url.path, + exc.detail, + exc_info=(type(exc), exc, exc.__traceback__), + ) + return await http_exception_handler(request, exc) + + @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): if isinstance(exc, HTTPException): sentry_sdk.capture_exception(exc) app_logger.error( - f"HTTPException {exc.status_code}: {exc.detail}", exc_info=True + "HTTPException %s on %s %s: %s", + exc.status_code, + request.method, + request.url.path, + exc.detail, + exc_info=(type(exc), exc, exc.__traceback__), ) raise exc else: # Non-HTTPException errors sentry_sdk.capture_exception(exc) - app_logger.error(f"Unexpected exception caught: {exc}", exc_info=True) + app_logger.error( + "Unexpected exception on %s %s: %s", + request.method, + request.url.path, + exc, + exc_info=True, + ) return JSONResponse( status_code=500, content={"detail": "Internal server error"} ) diff --git a/src/tests/unit/test_logging.py b/src/tests/unit/test_logging.py new file mode 100644 index 0000000..f84bb6f --- /dev/null +++ b/src/tests/unit/test_logging.py @@ -0,0 +1,35 @@ +import logging + +import pytest +from fastapi import HTTPException +from starlette.requests import Request + +from src.main import log_http_exception + + +@pytest.mark.asyncio +async def test_http_exception_logs_detail(caplog): + request = Request( + { + "type": "http", + "method": "GET", + "path": "/resource-server/groups/", + "headers": [], + "query_string": b"", + "server": ("testserver", 80), + "scheme": "http", + "client": ("testclient", 50000), + } + ) + exception = HTTPException( + status_code=401, + detail="Token does not contain 'sub' claim", + headers={"WWW-Authenticate": "Bearer"}, + ) + + with caplog.at_level(logging.ERROR, logger="src"): + response = await log_http_exception(request, exception) + + assert response.status_code == 401 + assert "HTTPException 401 on GET /resource-server/groups/" in caplog.text + assert "Token does not contain 'sub' claim" in caplog.text