Skip to content

FastAPI exception handlers: properly handled exceptions logged as errors in Logfire #1361

@renaudcepre

Description

@renaudcepre

Question

FastAPI exception handlers: properly handled domain exceptions logged as errors in Logfire

Summary

When using Logfire's instrument_fastapi(), exceptions that are properly handled by FastAPI exception handlers are still logged as errors in Logfire, even though they result in valid HTTP responses (404, 403, etc.). This pollutes error logs with normal business logic cases.

Expected Behavior

Exceptions that are handled by FastAPI exception handlers and converted to proper HTTP responses (4xx status codes) should NOT be logged as errors in Logfire. They should either not be logged as exceptions at all

Actual Behavior

All exceptions are logged as errors in Logfire, regardless of whether they're handled by exception handlers.

Code Example

Exception Handler Setup:

from fastapi import FastAPI
from fastapi.responses import JSONResponse
from generic_rag.domain.exceptions.base import DomainError

@app.exception_handler(DomainError)
async def domain_error_handler(request: Request, exc: DomainError) -> Response:
    return JSONResponse(status_code=404, content={"detail": str(exc)})

# Logfire instrumentation
logfire.instrument_fastapi(app, capture_headers=True)

Route that raises handled exception:

@router.get("/users/{user_id}")
def get_user(user_id: UUID) -> User:
    # This raises UserNotFoundError which inherits from DomainError
    return domain.get_user_for_company(user_id, api_user)

Result in Logfire

{
  "is_exception": true,
  "exception_type": "generic_rag.domain.exceptions.users.UserNotFoundError", 
  "otel_status_code": "ERROR",
  "http_response_status_code": 404
}
Image

The HTTP response is correctly 404, but Logfire still logs it as an error.

Workaround

Currently, the only workaround is to wrap every route in try/catch, which contradicts the use of error handler:

@router.get("/users/{user_id}")  
def get_user(user_id: UUID) -> User:
    try:
        return domain.get_user_for_company(user_id, api_user)
    except DomainError as exc:
        # Handle manually to avoid Logfire logging as error
        return JSONResponse(status_code=404, content={"detail": str(exc)})

With this approach:

{
  "is_exception": false,
  "otel_status_code": "UNSET", 
  "http_response_status_code": 404
}
Image

Questions

Am I missing something obvious here? I've searched through the documentation and tried different approaches, but couldn't find a clean solution that preserves the benefits of FastAPI's exception handler pattern while avoiding the error log pollution.

Environment

>>> import logfire; print(logfire.logfire_info())
logfire="4.3.3"
python="3.11.9 (main, Aug 14 2024, 05:07:28) [Clang 18.1.8 ]"
[related_packages]
requests="2.32.4"
pydantic="2.11.7"
fastapi="0.104.1"
openai="1.97.2"
protobuf="5.29.5"
rich="14.0.0"
executing="2.2.0"
opentelemetry-api="1.36.0"
opentelemetry-exporter-otlp="1.36.0"
opentelemetry-exporter-otlp-proto-common="1.36.0"
opentelemetry-exporter-otlp-proto-grpc="1.36.0"
opentelemetry-exporter-otlp-proto-http="1.36.0"
opentelemetry-instrumentation="0.57b0"
opentelemetry-instrumentation-asgi="0.57b0"
opentelemetry-instrumentation-celery="0.57b0"
opentelemetry-instrumentation-fastapi="0.57b0"
opentelemetry-instrumentation-llamaindex="0.45.6"
opentelemetry-instrumentation-sqlalchemy="0.57b0"
opentelemetry-proto="1.36.0"
opentelemetry-sdk="1.36.0"
opentelemetry-semantic-conventions="0.57b0"
opentelemetry-semantic-conventions-ai="0.4.12"
opentelemetry-util-http="0.57b0"

Sorry for the images that don't exactly match the snippets.

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions