diff --git a/README.md b/README.md index fda825f..c35c265 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ The application uses SQLite as the database. The database file is created at `/a ## CORS Configuration -The API has CORS (Cross-Origin Resource Sharing) enabled with the following configuration: +The API has robust CORS (Cross-Origin Resource Sharing) enabled with the following configuration: - Allowed origins: - http://localhost @@ -200,17 +200,43 @@ The API has CORS (Cross-Origin Resource Sharing) enabled with the following conf - * (wildcard for development) - Allowed methods: GET, POST, PUT, DELETE, OPTIONS, PATCH -- Allowed headers: Content-Type, Authorization, Accept, Origin, X-Requested-With, X-CSRF-Token, Access-Control-Allow-Credentials -- Exposed headers: Content-Length, Content-Type -- Credentials support: Enabled -- Max age for preflight requests: 600 seconds (10 minutes) +- Allowed 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-HTTP-Method-Override + +- Exposed headers: Content-Length, Content-Type, Authorization +- Credentials support: Enabled (supports JWT authentication) +- Max age for preflight requests: 3600 seconds (1 hour) ### Custom CORS Handling -This application uses both FastAPI's built-in CORSMiddleware and a custom CORS middleware that provides enhanced handling of preflight OPTIONS requests. The custom middleware also supports wildcard pattern matching for origins (e.g., https://*.vercel.app) to better support deployment platforms. +This application implements a custom CORS middleware that properly handles preflight OPTIONS requests for all endpoints, including authentication routes. The middleware includes: + +1. Direct handling of OPTIONS requests for all endpoints +2. Proper header handling for preflight responses +3. Explicit support for POST requests with JSON content-type +4. Full support for Authorization headers for authenticated endpoints +5. Pattern matching for wildcard domains (e.g., *.vercel.app) + +### CORS Test Endpoint + +The API includes a special endpoint for testing CORS functionality: + +- `OPTIONS /api/v1/cors-test` - Test preflight requests +- `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 | False | \ No newline at end of file +| USE_CUSTOM_CORS_ONLY | Whether to use only the custom CORS middleware | True | \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 1afb934..d3f4024 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -41,7 +41,7 @@ class Settings(BaseSettings): ] # Whether to use only the custom CORS middleware - USE_CUSTOM_CORS_ONLY: bool = False + USE_CUSTOM_CORS_ONLY: bool = True # Security settings PASSWORD_HASH_ROUNDS: int = 12 diff --git a/main.py b/main.py index a3b1853..36ac595 100644 --- a/main.py +++ b/main.py @@ -19,45 +19,69 @@ class CustomCORSMiddleware(BaseHTTPMiddleware): if origin in settings.CORS_ORIGINS: return True - # Wildcard match + # Wildcard match - if "*" is in the allowed origins list if "*" in settings.CORS_ORIGINS: return True # Check for pattern matching (e.g., https://*.vercel.app) for allowed_origin in settings.CORS_ORIGINS: - if "*" in allowed_origin: - pattern = allowed_origin.replace("*", "") - if origin.startswith(pattern.split("*")[0]) and origin.endswith(pattern.split("*")[-1]): - return True + 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 async def dispatch(self, request: Request, call_next): origin = request.headers.get("origin", "") + # Always respond to OPTIONS requests directly for preflight handling if request.method == "OPTIONS": - # Handle preflight requests - response = Response() + # Create a new response for preflight + response = Response(status_code=204) # No content needed for preflight + + # If no origin or not allowed, return 204 with minimal headers + # This will not block the request but won't allow CORS either + if not origin or not self.is_origin_allowed(origin): + return response + + # If origin is allowed, set the full CORS headers + response.headers["Access-Control-Allow-Origin"] = origin + + # 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 - # Check if the origin is allowed - if self.is_origin_allowed(origin): - response.headers["Access-Control-Allow-Origin"] = origin - response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS, PATCH" - response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, Accept, Origin, X-Requested-With, X-CSRF-Token, Access-Control-Allow-Credentials" - response.headers["Access-Control-Allow-Credentials"] = "true" - response.headers["Access-Control-Max-Age"] = "600" - response.status_code = 200 - return response - # Process regular requests + # For regular requests, process normally then add CORS headers response = await call_next(request) - # Set CORS headers for the response + # 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( @@ -92,5 +116,29 @@ app.include_router(api_router) async def health_check(): return {"status": "healthy"} +# CORS test endpoint +@app.options("/api/v1/cors-test", tags=["cors"]) +async def cors_preflight_test(): + """Test endpoint for CORS preflight requests.""" + return None + +@app.post("/api/v1/cors-test", tags=["cors"]) +async def cors_test(request: Request): + """Test endpoint for CORS POST requests with JSON.""" + try: + body = await request.json() + return { + "success": True, + "message": "CORS is working correctly for POST requests with JSON", + "received_data": body, + "headers": dict(request.headers) + } + except Exception as e: + return { + "success": False, + "message": f"Error: {str(e)}", + "headers": dict(request.headers) + } + if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file