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 |

183
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 async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
# Pass through other types of requests (WebSocket, lifespan)
await self.app(scope, receive, send)
return
# Get request info
method = scope.get("method", "")
headers = dict(scope.get("headers", []))
# Convert byte headers to strings
origin = None
if b'origin' in headers:
origin = headers[b'origin'].decode('utf-8')
# Direct match # Handle OPTIONS requests for CORS preflight
if origin in settings.CORS_ORIGINS: if method == "OPTIONS":
return True async def send_preflight_response(message):
if message["type"] == "http.response.start":
# Wildcard match - if "*" is in the allowed origins list # Create a custom response for preflight
if "*" in settings.CORS_ORIGINS: headers = [
return True (b"content-type", b"text/plain"),
(b"content-length", b"0"),
# Check for pattern matching (e.g., https://*.vercel.app) ]
for allowed_origin in settings.CORS_ORIGINS:
if "*" in allowed_origin and not allowed_origin == "*":
pattern_parts = allowed_origin.split("*")
if len(pattern_parts) == 2:
if origin.startswith(pattern_parts[0]) and origin.endswith(pattern_parts[1]):
return True
return False # Add CORS headers
if origin:
async def dispatch(self, request: Request, call_next): headers.extend([
origin = request.headers.get("origin", "") (b"access-control-allow-origin", origin.encode()),
(b"access-control-allow-credentials", b"true"),
# Always respond to OPTIONS requests directly for preflight handling (b"access-control-allow-methods", b"GET, POST, PUT, DELETE, OPTIONS, PATCH".encode()),
if request.method == "OPTIONS": (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"),
# Create a new response for preflight (b"access-control-max-age", b"3600"),
response = Response(status_code=204) # No content needed for preflight (b"vary", b"Origin"),
])
# Send the response
await send({
"type": "http.response.start",
"status": 200,
"headers": headers
})
else:
await send(message)
# Handle the preflight request with our custom response
await send_preflight_response({"type": "http.response.start"})
await send({"type": "http.response.body", "body": b""})
return
# If no origin or not allowed, return 204 with minimal headers # For non-OPTIONS requests, wrap the send function to add CORS headers
# This will not block the request but won't allow CORS either async def cors_send(message):
if not origin or not self.is_origin_allowed(origin): if message["type"] == "http.response.start":
return response # Get original headers
headers = list(message.get("headers", []))
# Add CORS headers if origin is present
if origin:
# Add CORS headers
headers.extend([
(b"access-control-allow-origin", origin.encode()),
(b"access-control-allow-credentials", b"true"),
(b"vary", b"Origin"),
])
# Send modified response
message["headers"] = headers
await send(message)
# If origin is allowed, set the full CORS headers # Process the request with CORS headers added to response
response.headers["Access-Control-Allow-Origin"] = origin await self.app(scope, receive, cors_send)
# Include all possible headers that might be used by the frontend
# Make sure Content-Type is included to support application/json
response.headers["Access-Control-Allow-Headers"] = (
"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-Requested-With, X-HTTP-Method-Override"
)
# Expose headers that frontend might need to access
response.headers["Access-Control-Expose-Headers"] = (
"Content-Length, Content-Type, Authorization"
)
response.headers["Access-Control-Allow-Credentials"] = "true"
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
# 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)