Fix CORS configuration to allow requests from Vercel frontend

This commit is contained in:
Automated Action 2025-06-05 11:19:55 +00:00
parent 1f546e5189
commit 1ceb141b46
2 changed files with 111 additions and 103 deletions

View File

@ -220,23 +220,26 @@ The API has robust CORS (Cross-Origin Resource Sharing) enabled with the followi
### Custom CORS Handling ### Custom CORS Handling
This application implements a custom CORS middleware that properly handles preflight OPTIONS requests for all endpoints, including authentication routes. The middleware includes: This application implements a low-level ASGI CORS middleware that properly handles preflight OPTIONS requests for all endpoints, including authentication routes. The implementation includes:
1. Direct handling of OPTIONS requests for all endpoints 1. Low-level ASGI middleware that directly handles HTTP requests before FastAPI routing
2. Proper header handling for preflight responses 2. Special handling for OPTIONS preflight requests for all routes
3. Explicit support for POST requests with JSON content-type 3. Explicit support for POST requests with JSON content-type
4. Full support for Authorization headers for authenticated endpoints 4. Full support for Authorization headers for authenticated endpoints
5. Pattern matching for wildcard domains (e.g., *.vercel.app) 5. Dedicated OPTIONS route handlers for critical endpoints like authentication
### CORS Test Endpoint The CORS system is implemented at multiple levels to ensure maximum compatibility:
The API includes a special endpoint for testing CORS functionality: 1. **ASGI Middleware**: Intercepts all requests at the ASGI protocol level before FastAPI processing
2. **Dedicated OPTIONS Handlers**: Specific route handlers for authentication endpoints
3. **Response Header Injection**: Adds proper CORS headers to all responses
### Critical Endpoints with Special CORS Support
The API includes dedicated OPTIONS handlers for these critical endpoints:
- `OPTIONS /api/v1/auth/register` - Register endpoint preflight support
- `OPTIONS /api/v1/auth/login` - Login endpoint preflight support
- `OPTIONS /api/v1/users/me` - User profile endpoint preflight support
- `OPTIONS /api/v1/cors-test` - Test preflight requests - `OPTIONS /api/v1/cors-test` - Test preflight requests
- `POST /api/v1/cors-test` - Test POST requests with JSON body - `POST /api/v1/cors-test` - Test POST requests with JSON body
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| USE_CUSTOM_CORS_ONLY | Whether to use only the custom CORS middleware | True |

167
main.py
View File

@ -1,88 +1,89 @@
import uvicorn import uvicorn
from fastapi import FastAPI, Request, Response from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from starlette.types import ASGIApp, Receive, Scope, Send
from starlette.middleware.base import BaseHTTPMiddleware
from app.api.v1.api import api_router from app.api.v1.api import api_router
from app.core.config import settings from app.core.config import settings
class CustomCORSMiddleware(BaseHTTPMiddleware): class CORSMiddlewareASGI:
"""Custom middleware to ensure CORS headers are set correctly for all responses.""" """A lower-level ASGI middleware for CORS that intercepts all requests."""
def is_origin_allowed(self, origin: str) -> bool: def __init__(self, app: ASGIApp):
"""Check if the origin is allowed based on the CORS_ORIGINS settings.""" self.app = app
if not origin:
return False
# Direct match async def __call__(self, scope: Scope, receive: Receive, send: Send):
if origin in settings.CORS_ORIGINS: if scope["type"] != "http":
return True # Pass through other types of requests (WebSocket, lifespan)
await self.app(scope, receive, send)
return
# Wildcard match - if "*" is in the allowed origins list # Get request info
if "*" in settings.CORS_ORIGINS: method = scope.get("method", "")
return True headers = dict(scope.get("headers", []))
# Check for pattern matching (e.g., https://*.vercel.app) # Convert byte headers to strings
for allowed_origin in settings.CORS_ORIGINS: origin = None
if "*" in allowed_origin and not allowed_origin == "*": if b'origin' in headers:
pattern_parts = allowed_origin.split("*") origin = headers[b'origin'].decode('utf-8')
if len(pattern_parts) == 2:
if origin.startswith(pattern_parts[0]) and origin.endswith(pattern_parts[1]):
return True
return False # Handle OPTIONS requests for CORS preflight
if method == "OPTIONS":
async def send_preflight_response(message):
if message["type"] == "http.response.start":
# Create a custom response for preflight
headers = [
(b"content-type", b"text/plain"),
(b"content-length", b"0"),
]
async def dispatch(self, request: Request, call_next): # Add CORS headers
origin = request.headers.get("origin", "") if origin:
headers.extend([
(b"access-control-allow-origin", origin.encode()),
(b"access-control-allow-credentials", b"true"),
(b"access-control-allow-methods", b"GET, POST, PUT, DELETE, OPTIONS, PATCH".encode()),
(b"access-control-allow-headers", b"Authorization, Content-Type, Accept, Accept-Language, Content-Language, Content-Length, Origin, X-Requested-With, X-CSRF-Token, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, X-HTTP-Method-Override"),
(b"access-control-max-age", b"3600"),
(b"vary", b"Origin"),
])
# Always respond to OPTIONS requests directly for preflight handling # Send the response
if request.method == "OPTIONS": await send({
# Create a new response for preflight "type": "http.response.start",
response = Response(status_code=204) # No content needed for preflight "status": 200,
"headers": headers
})
else:
await send(message)
# If no origin or not allowed, return 204 with minimal headers # Handle the preflight request with our custom response
# This will not block the request but won't allow CORS either await send_preflight_response({"type": "http.response.start"})
if not origin or not self.is_origin_allowed(origin): await send({"type": "http.response.body", "body": b""})
return response return
# If origin is allowed, set the full CORS headers # For non-OPTIONS requests, wrap the send function to add CORS headers
response.headers["Access-Control-Allow-Origin"] = origin async def cors_send(message):
if message["type"] == "http.response.start":
# Get original headers
headers = list(message.get("headers", []))
# Include all possible headers that might be used by the frontend # Add CORS headers if origin is present
# Make sure Content-Type is included to support application/json if origin:
response.headers["Access-Control-Allow-Headers"] = ( # Add CORS headers
"Authorization, Content-Type, Accept, Accept-Language, " + headers.extend([
"Content-Language, Content-Length, Origin, X-Requested-With, " + (b"access-control-allow-origin", origin.encode()),
"X-CSRF-Token, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, " + (b"access-control-allow-credentials", b"true"),
"X-Requested-With, X-HTTP-Method-Override" (b"vary", b"Origin"),
) ])
# Expose headers that frontend might need to access # Send modified response
response.headers["Access-Control-Expose-Headers"] = ( message["headers"] = headers
"Content-Length, Content-Type, Authorization"
)
response.headers["Access-Control-Allow-Credentials"] = "true" await send(message)
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS, PATCH"
response.headers["Access-Control-Max-Age"] = "3600" # 1 hour cache
response.status_code = 200 # OK for successful preflight
return response # Process the request with CORS headers added to response
await self.app(scope, receive, cors_send)
# For regular requests, process normally then add CORS headers
response = await call_next(request)
# Add CORS headers to all responses if origin is allowed
if self.is_origin_allowed(origin):
# Set required CORS headers
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
# Add Vary header to indicate caching should consider Origin
response.headers["Vary"] = "Origin"
return response
app = FastAPI( app = FastAPI(
title=settings.PROJECT_NAME, title=settings.PROJECT_NAME,
@ -93,20 +94,8 @@ app = FastAPI(
redoc_url="/redoc", redoc_url="/redoc",
) )
# Add our custom CORS middleware first (higher priority) # Remove all middleware and mount the main app with our ASGI middleware
app.add_middleware(CustomCORSMiddleware) app = CORSMiddlewareASGI(app)
# Set up standard CORS middleware as a backup if not disabled
if not settings.USE_CUSTOM_CORS_ONLY:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["Content-Type", "Authorization", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token", "Access-Control-Allow-Credentials"],
expose_headers=["Content-Length", "Content-Type"],
max_age=600, # 10 minutes cache for preflight requests
)
# Include API router # Include API router
app.include_router(api_router) app.include_router(api_router)
@ -116,11 +105,11 @@ app.include_router(api_router)
async def health_check(): async def health_check():
return {"status": "healthy"} return {"status": "healthy"}
# CORS test endpoint # CORS test endpoints
@app.options("/api/v1/cors-test", tags=["cors"]) @app.options("/api/v1/cors-test", tags=["cors"])
async def cors_preflight_test(): async def cors_preflight_test():
"""Test endpoint for CORS preflight requests.""" """Test endpoint for CORS preflight requests."""
return None return Response(status_code=200)
@app.post("/api/v1/cors-test", tags=["cors"]) @app.post("/api/v1/cors-test", tags=["cors"])
async def cors_test(request: Request): async def cors_test(request: Request):
@ -140,5 +129,21 @@ async def cors_test(request: Request):
"headers": dict(request.headers) "headers": dict(request.headers)
} }
# Additional OPTIONS handlers for critical endpoints
@app.options("/api/v1/auth/register", include_in_schema=False)
async def auth_register_options():
"""Handle OPTIONS preflight requests for auth register endpoint."""
return Response(status_code=200)
@app.options("/api/v1/auth/login", include_in_schema=False)
async def auth_login_options():
"""Handle OPTIONS preflight requests for auth login endpoint."""
return Response(status_code=200)
@app.options("/api/v1/users/me", include_in_schema=False)
async def users_me_options():
"""Handle OPTIONS preflight requests for users/me endpoint."""
return Response(status_code=200)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)