Add request validation and error handling middleware

- Created RequestValidationMiddleware for request/response size validation
- Created ErrorHandlerMiddleware for standardized error responses
- Added both middlewares to FastAPI app
- Created validation patterns documentation
- Configured 10MB default size limits for requests and responses

This completes Phase 6: Request Validation Middleware
This commit is contained in:
aitbc
2026-04-30 11:08:39 +02:00
parent 4c26f742dc
commit 56d510e75f
4 changed files with 353 additions and 0 deletions

View File

@@ -94,6 +94,8 @@ from aitbc import get_logger
from .app_logging import configure_logging
from .middleware.request_id import RequestIDMiddleware
from .middleware.performance import PerformanceLoggingMiddleware
from .middleware.validation import RequestValidationMiddleware
from .middleware.error_handler import ErrorHandlerMiddleware
from .exceptions import AITBCError, ErrorResponse
# Configure structured logging
@@ -293,6 +295,12 @@ def create_app() -> FastAPI:
# Add performance logging middleware
app.add_middleware(PerformanceLoggingMiddleware)
# Add request validation middleware
app.add_middleware(RequestValidationMiddleware, max_request_size=10*1024*1024)
# Add error handler middleware
app.add_middleware(ErrorHandlerMiddleware)
@app.middleware("http")
async def request_metrics_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
start_time = __import__("time").perf_counter()

View File

@@ -0,0 +1,61 @@
"""
Standardized error response middleware for FastAPI
"""
from typing import Callable
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from ..app_logging import get_logger
logger = get_logger(__name__)
class ErrorHandlerMiddleware(BaseHTTPMiddleware):
"""Middleware to standardize error responses"""
async def dispatch(self, request: Request, call_next: Callable) -> JSONResponse:
try:
response = await call_next(request)
return response
except HTTPException as e:
logger.warning(
"HTTP exception",
status_code=e.status_code,
detail=e.detail,
path=request.url.path,
method=request.method,
)
return JSONResponse(
status_code=e.status_code,
content={
"error": {
"type": "http_error",
"message": e.detail,
"status_code": e.status_code,
"path": request.url.path,
}
},
)
except Exception as e:
logger.error(
"Unhandled exception",
error=str(e),
path=request.url.path,
method=request.method,
exc_info=True,
)
return JSONResponse(
status_code=500,
content={
"error": {
"type": "internal_error",
"message": "An internal server error occurred",
"status_code": 500,
"path": request.url.path,
}
},
)

View File

@@ -0,0 +1,66 @@
"""
Request validation middleware for FastAPI
"""
from typing import Callable
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from ..app_logging import get_logger
logger = get_logger(__name__)
class RequestValidationMiddleware(BaseHTTPMiddleware):
"""Middleware to validate incoming requests"""
def __init__(
self,
app: ASGIApp,
max_request_size: int = 10 * 1024 * 1024, # 10MB default
max_response_size: int = 10 * 1024 * 1024, # 10MB default
) -> None:
super().__init__(app)
self.max_request_size = max_request_size
self.max_response_size = max_response_size
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Validate request size
content_length = request.headers.get("content-length")
if content_length:
try:
size = int(content_length)
if size > self.max_request_size:
logger.warning(
"Request too large",
content_length=size,
max_size=self.max_request_size,
client=request.client.host if request.client else "unknown",
)
raise HTTPException(
status_code=413,
detail=f"Request too large. Maximum size is {self.max_request_size} bytes",
)
except ValueError:
logger.warning("Invalid content-length header", content_length=content_length)
# Process request
response = await call_next(request)
# Validate response size
response_size = len(response.body)
if response_size > self.max_response_size:
logger.warning(
"Response too large",
response_size=response_size,
max_size=self.max_response_size,
path=request.url.path,
)
raise HTTPException(
status_code=500,
detail="Response too large",
)
return response

View File

@@ -0,0 +1,218 @@
# Request Validation Patterns
## Overview
This document describes the request validation patterns and middleware for AITBC services.
## Middleware Stack
The coordinator-api uses the following middleware stack (in order of execution):
1. **CORSMiddleware** - CORS handling
2. **RequestIDMiddleware** - Request ID correlation
3. **PerformanceLoggingMiddleware** - Performance tracking
4. **RequestValidationMiddleware** - Request/response size validation
5. **ErrorHandlerMiddleware** - Standardized error responses
## Request Validation Middleware
### Purpose
Validates incoming and outgoing requests/responses to ensure they meet size limits and other constraints.
### Configuration
```python
app.add_middleware(
RequestValidationMiddleware,
max_request_size=10*1024*1024, # 10MB
)
```
### Validation Rules
- **Request size**: Maximum 10MB by default
- **Response size**: Maximum 10MB by default
- **Content-Length header**: Must be valid integer if present
### Error Responses
If validation fails, returns HTTP 413 (Payload Too Large):
```json
{
"detail": "Request too large. Maximum size is 10485760 bytes"
}
```
## Error Handler Middleware
### Purpose
Standardizes error responses across all endpoints with consistent format and logging.
### Error Response Format
All errors are returned in the following format:
```json
{
"error": {
"type": "http_error | internal_error",
"message": "Error description",
"status_code": 400 | 500,
"path": "/api/endpoint"
}
}
```
### Error Types
- **http_error**: HTTP exceptions (4xx errors)
- **internal_error**: Unhandled exceptions (5xx errors)
### Logging
All errors are logged with context:
- HTTP exceptions: WARNING level
- Internal exceptions: ERROR level with stack trace
## Request ID Correlation
### Purpose
Adds a unique request ID to each request for correlation across distributed systems.
### Implementation
- Generates or retrieves request ID from `X-Request-ID` header
- Binds request ID to logger context
- Adds request ID to response headers
- Logs request start and completion
### Usage
```python
request_id = request.state.request_id
logger = logger.bind(request_id=request_id)
```
## Performance Logging
### Purpose
Tracks request timing and performance metrics.
### Implementation
- Logs request duration in milliseconds
- Adds `X-Process-Time` header to responses
- Logs method, path, and status code
### Response Header
```
X-Process-Time: 0.123
```
## Validation Guidelines
### DO
- Use Pydantic models for request body validation
- Set appropriate size limits for your use case
- Log validation failures with context
- Return standardized error responses
- Include request ID in error logs
### DON'T
- Disable validation in production
- Allow unbounded request sizes
- Return raw exceptions to clients
- Log sensitive information in error messages
- Skip error logging
## Example: Adding Custom Validation
```python
from fastapi import Request, HTTPException
from pydantic import BaseModel, Field, validator
class CreateUserRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., regex=r'^[^@]+@[^@]+\.[^@]+$')
@validator('username')
def validate_username(cls, v):
if not v.isalnum():
raise ValueError('Username must be alphanumeric')
return v
@router.post("/users")
async def create_user(request: CreateUserRequest):
# Request is automatically validated by FastAPI
return {"status": "created", "username": request.username}
```
## Rate Limiting
Rate limiting is already implemented using slowapi:
```python
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.post("/endpoint")
@limiter.limit("10/minute")
async def endpoint(request: Request):
return {"status": "ok"}
```
## Configuration
Middleware can be configured in `main.py`:
```python
# Request validation
app.add_middleware(
RequestValidationMiddleware,
max_request_size=10*1024*1024, # 10MB
max_response_size=10*1024*1024, # 10MB
)
# Error handling
app.add_middleware(ErrorHandlerMiddleware)
```
## Testing
Test validation middleware:
```python
from fastapi.testclient import TestClient
client = TestClient(app)
# Test request size validation
response = client.post(
"/endpoint",
data="x" * 11 * 1024 * 1024, # 11MB
)
assert response.status_code == 413
# Test error handling
response = client.get("/nonexistent")
assert response.status_code == 404
assert "error" in response.json()
```
## Security Considerations
- Request size limits prevent DoS attacks
- Response size limits prevent memory exhaustion
- Error responses don't leak sensitive information
- Request IDs enable security audit trails
- All validation failures are logged for monitoring