|
7 | 7 |
|
8 | 8 | from dotenv import load_dotenv |
9 | 9 | from fastapi import FastAPI, Request, HTTPException |
10 | | -from fastapi.responses import FileResponse, HTMLResponse |
| 10 | +from fastapi.exceptions import RequestValidationError |
| 11 | +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse |
11 | 12 | from fastapi.staticfiles import StaticFiles |
12 | 13 | from fastapi.templating import Jinja2Templates |
13 | 14 | from slowapi.errors import RateLimitExceeded |
14 | 15 | from starlette.middleware.trustedhost import TrustedHostMiddleware |
| 16 | +from starlette.middleware.base import BaseHTTPMiddleware |
15 | 17 |
|
16 | 18 | from server.routers import index, dynamic |
17 | 19 | from server.server_config import templates |
|
28 | 30 | app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler) |
29 | 31 |
|
30 | 32 |
|
| 33 | +@app.exception_handler(RequestValidationError) |
| 34 | +async def validation_exception_handler(request: Request, exc: RequestValidationError): |
| 35 | + """ |
| 36 | + Handle validation errors (422) with detailed logging for debugging. |
| 37 | + |
| 38 | + This helps diagnose environmental issues like missing form data or incorrect Content-Type headers. |
| 39 | + """ |
| 40 | + # Log the error details for debugging |
| 41 | + host = request.headers.get("host", "unknown") |
| 42 | + content_type = request.headers.get("content-type", "unknown") |
| 43 | + method = request.method |
| 44 | + path = request.url.path |
| 45 | + |
| 46 | + error_details = { |
| 47 | + "host": host, |
| 48 | + "content_type": content_type, |
| 49 | + "method": method, |
| 50 | + "path": path, |
| 51 | + "errors": exc.errors(), |
| 52 | + } |
| 53 | + |
| 54 | + print(f"ERROR: 422 Validation Error - Host: {host}, Content-Type: {content_type}, Path: {path}") |
| 55 | + print(f"ERROR: Validation details: {error_details}") |
| 56 | + |
| 57 | + # If it's a form submission, return a helpful error message |
| 58 | + if method == "POST" and "form" in str(exc.errors()).lower(): |
| 59 | + return JSONResponse( |
| 60 | + status_code=422, |
| 61 | + content={ |
| 62 | + "detail": "Form validation failed. Make sure the request has Content-Type: application/x-www-form-urlencoded or multipart/form-data", |
| 63 | + "host": host, |
| 64 | + "content_type": content_type, |
| 65 | + "errors": exc.errors(), |
| 66 | + } |
| 67 | + ) |
| 68 | + |
| 69 | + # Otherwise return standard validation error |
| 70 | + return JSONResponse( |
| 71 | + status_code=422, |
| 72 | + content={"detail": exc.errors(), "body": exc.body} |
| 73 | + ) |
| 74 | + |
| 75 | + |
31 | 76 | # Mount static files dynamically to serve CSS, JS, and other static assets |
32 | 77 | static_dir = Path(__file__).parent.parent / "static" |
33 | 78 | app.mount("/static", StaticFiles(directory=static_dir), name="static") |
|
37 | 82 | allowed_hosts = os.getenv("ALLOWED_HOSTS") |
38 | 83 | if allowed_hosts: |
39 | 84 | allowed_hosts = allowed_hosts.split(",") |
| 85 | + # Strip whitespace from each host |
| 86 | + allowed_hosts = [host.strip() for host in allowed_hosts] |
40 | 87 | else: |
41 | 88 | # Define the default allowed hosts for the application |
42 | 89 | default_allowed_hosts = [ |
|
46 | 93 | ] |
47 | 94 | allowed_hosts = default_allowed_hosts |
48 | 95 |
|
| 96 | +# Log allowed hosts for debugging (only in development) |
| 97 | +if os.getenv("DEBUG", "").lower() in ("true", "1"): |
| 98 | + print(f"DEBUG: Allowed hosts: {allowed_hosts}") |
| 99 | + |
| 100 | + |
| 101 | +class RequestLoggingMiddleware(BaseHTTPMiddleware): |
| 102 | + """Middleware to log request details for debugging 422 errors.""" |
| 103 | + |
| 104 | + async def dispatch(self, request: Request, call_next): |
| 105 | + # Log POST requests that might fail validation |
| 106 | + if request.method == "POST": |
| 107 | + host = request.headers.get("host", "unknown") |
| 108 | + content_type = request.headers.get("content-type", "missing") |
| 109 | + path = request.url.path |
| 110 | + |
| 111 | + # Only log if DEBUG is enabled or if content-type is suspicious |
| 112 | + if os.getenv("DEBUG", "").lower() in ("true", "1") or not content_type.startswith(("application/x-www-form-urlencoded", "multipart/form-data")): |
| 113 | + print(f"DEBUG POST: Host={host}, Content-Type={content_type}, Path={path}") |
| 114 | + |
| 115 | + response = await call_next(request) |
| 116 | + return response |
| 117 | + |
| 118 | + |
| 119 | +# Add request logging middleware first (runs last in the chain) |
| 120 | +app.add_middleware(RequestLoggingMiddleware) |
| 121 | + |
49 | 122 | # Add middleware to enforce allowed hosts |
| 123 | +# Note: TrustedHostMiddleware can cause 400 errors if Host header doesn't match |
| 124 | +# Make sure ALLOWED_HOSTS environment variable includes your actual domain |
| 125 | +# If you're behind a proxy, you may need to configure X-Forwarded-Host handling |
50 | 126 | app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts) |
51 | 127 |
|
52 | 128 |
|
|
0 commit comments