Add unified RequestValidationError handler to unify error response schema
- Add RequestValidationError handler that converts Pydantic validation errors to unified ErrorResponse format - Ensures all error responses return consistent schema: code, detail, metadata, correlation_id - Add field_errors count and first_field location to metadata for validation errors - Register handler in exception handler hierarchy before HTTPException handler - Add comprehensive tests for validation error responses - Update Backend-Development.md documentation to include correlation_id field and validation error details - All 44 error-related tests pass (38 existing + 6 new validation tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
||||
|
||||
import structlog
|
||||
from fastapi import FastAPI, HTTPException, Request, status
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
@@ -621,6 +622,50 @@ async def _http_exception_handler(
|
||||
)
|
||||
|
||||
|
||||
async def _request_validation_error_handler(
|
||||
request: Request,
|
||||
exc: RequestValidationError,
|
||||
) -> JSONResponse:
|
||||
"""Return a standardized error response for Pydantic validation errors.
|
||||
|
||||
Converts FastAPI's RequestValidationError to our unified ErrorResponse format.
|
||||
Aggregates validation errors into metadata for the client to handle.
|
||||
|
||||
Args:
|
||||
request: The incoming FastAPI request.
|
||||
exc: The :class:`fastapi.exceptions.RequestValidationError`.
|
||||
|
||||
Returns:
|
||||
A :class:`fastapi.responses.JSONResponse` with status 400.
|
||||
"""
|
||||
log.warning(
|
||||
"request_validation_error",
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
error_count=len(exc.errors()),
|
||||
)
|
||||
|
||||
validation_errors = exc.errors()
|
||||
error_details: dict[str, str | int | float | bool | None] = {}
|
||||
|
||||
if validation_errors:
|
||||
error_details["field_errors"] = len(validation_errors)
|
||||
first_error = validation_errors[0]
|
||||
error_details["first_field"] = ".".join(str(x) for x in first_error["loc"])
|
||||
|
||||
error_response = ErrorResponse(
|
||||
code="invalid_input",
|
||||
detail="Request validation failed.",
|
||||
metadata=error_details,
|
||||
correlation_id=_get_correlation_id(request),
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content=error_response.model_dump(),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Setup-redirect middleware
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -854,9 +899,10 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
# 4. OperationError handler → HTTP 500
|
||||
# 5. ServiceUnavailableError handler → HTTP 503
|
||||
# 6. Generic DomainError handler (catch-all for any unregistered DomainError subclass) → HTTP 500
|
||||
# 7. HTTPException (FastAPI built-ins, validation errors) → HTTP varies
|
||||
# 8. ValueError (Pydantic validation) → HTTP 400
|
||||
# 9. Exception (absolute catch-all for unexpected errors) → HTTP 500
|
||||
# 7. RequestValidationError handler (Pydantic validation errors) → HTTP 400
|
||||
# 8. HTTPException (FastAPI built-ins) → HTTP varies
|
||||
# 9. ValueError (Pydantic validation) → HTTP 400
|
||||
# 10. Exception (absolute catch-all for unexpected errors) → HTTP 500
|
||||
#
|
||||
# This ensures that any new DomainError subclass that inherits from a registered category
|
||||
# is automatically handled with the correct error_code and metadata. If a developer adds
|
||||
@@ -872,6 +918,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
app.add_exception_handler(OperationError, _domain_error_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(ServiceUnavailableError, _service_unavailable_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(DomainError, _domain_error_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(RequestValidationError, _request_validation_error_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(HTTPException, _http_exception_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(ValueError, _value_error_handler) # type: ignore[arg-type]
|
||||
app.add_exception_handler(Exception, _unhandled_exception_handler)
|
||||
|
||||
Reference in New Issue
Block a user