mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user