feat(api/middleware): log request body + per-field errors on 422 validation failures

FastAPI's default RequestValidationError returns details to the client but nothing to server logs. Smoke test hit a 422 on /api/v2/flow/main_pm/complete with no way to tell which field failed. Add a handler that logs path/method/body/errors on every 422 so the next failure is debuggable in one log scan.
This commit is contained in:
Renn F
2026-05-02 04:55:21 +02:00
parent 33464a207a
commit de54c3b52d
+29 -3
View File
@@ -12,6 +12,7 @@ from typing import cast
import structlog import structlog
from fastapi import FastAPI, HTTPException, Request, Response from fastapi import FastAPI, HTTPException, Request, Response
from fastapi import status as http_status from fastapi import status as http_status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
@@ -239,6 +240,29 @@ async def http_exception_handler(request: Request, exc: Exception) -> JSONRespon
# ============================================================================= # =============================================================================
async def request_validation_handler(request: Request, exc: Exception) -> JSONResponse:
"""Log the rejected body before returning the standard 422 response.
FastAPI's default 422 returns validation details to the client but
nothing lands in server logs. During smoke tests this leaves us
blind to which field actually broke. Log the body + the per-field
errors so the next 422 is debuggable in one log scan.
"""
rve = cast("RequestValidationError", exc)
body = rve.body if isinstance(rve.body, str | bytes | dict | list) else None
logger.warning(
"Request validation failed",
path=request.url.path,
method=request.method,
body=body,
errors=rve.errors(),
)
return JSONResponse(
status_code=http_status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": rve.errors(), "body": body},
)
def setup_middleware(app: FastAPI) -> None: def setup_middleware(app: FastAPI) -> None:
""" """
Setup all middleware for the application. Setup all middleware for the application.
@@ -248,11 +272,13 @@ def setup_middleware(app: FastAPI) -> None:
2. RequestLoggingMiddleware - logs with correlation ID 2. RequestLoggingMiddleware - logs with correlation ID
Exception handler priority: Exception handler priority:
1. HTTPException - most common, converts to string error codes 1. RequestValidationError - 422s; log body + per-field errors
2. RobocoError - custom domain exceptions 2. HTTPException - most common, converts to string error codes
3. Exception - catch-all for unexpected errors 3. RobocoError - custom domain exceptions
4. Exception - catch-all for unexpected errors
""" """
# Exception handlers (order: specific to general) # Exception handlers (order: specific to general)
app.add_exception_handler(RequestValidationError, request_validation_handler)
app.add_exception_handler(HTTPException, http_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RobocoError, roboco_exception_handler) app.add_exception_handler(RobocoError, roboco_exception_handler)
app.add_exception_handler(Exception, generic_exception_handler) app.add_exception_handler(Exception, generic_exception_handler)