I'm implementing a FastAPI backend where I want to allow CORS requests only from a predefined list of default origins and dynamically loaded origins stored in the database.
However, when the frontend tries to call a GET API, the browser first sends an OPTIONS preflight request (as expected for CORS). The problem is:
The OPTIONS request fails due to a CORS error.
As a result, the actual GET request is never made.
This issue suggests that the CORS middleware is not properly handling the dynamic origins, especially during the OPTIONS preflight phase.
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette.middleware.base import BaseHTTPMiddleware,RequestResponseEndpoint
origins = [
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:8006",
]
class DynamicCORSMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
origin = request.headers.get("origin")
ic(origin)
ic(request.method)
# Preflight request
if request.method == "OPTIONS":
response = JSONResponse(content={"status": "ok"})
else:
response = await call_next(request)
# If no Origin header, skip CORS (non-browser requests)
if not origin:
return response
if origin in origins:
ic("Origin in default origins")
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Vary"] = "Origin"
return response
# Extract domain (remove protocol and port)
ic(origin)
domain = origin.replace("https://", "").replace("http://", "")
ic(domain)
# Check if domain exists in database
domain_exists = domain_collection.find_one({"$or": [{"domain.main_domain": domain}, {"domain.sub_domain": domain}]})
if not domain_exists:
raise HTTPException(
status_code=403,
detail=f"Domain {domain} not authorized"
)
ic(domain_exists)
# Process request
response = await call_next(request)
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Vary"] = "Origin"
ic(response)
return response
enter code here
app = FastAPI(
title="API",
description="API documentation for backend",
version="1.0.0",
docs_url=None, # Disable the default docs URL
redoc_url=None # Disable the default redoc URL
)
app.add_middleware(DynamicCORSMiddleware)
I have already explained the issue on comment. Here is the code with solution:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.types import ASGIApp
from pymongo import MongoClient
import re
client = MongoClient("mongo_client_url")
domain_collection = client.your_db.your_collection
origins = [
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:8006",
]
class DynamicCORSMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
origin = request.headers.get("origin")
allow_origin = None
if origin in origins:
allow_origin = origin
else:
domain = re.sub(r"https?://", "", origin or "")
domain = domain.split(":")[0]
if domain:
domain_exists = domain_collection.find_one({
"$or": [
{"domain.main_domain": domain},
{"domain.sub_domain": domain}
]
})
if domain_exists:
allow_origin = origin
if request.method == "OPTIONS":
if allow_origin:
return self._cors_preflight_response(allow_origin)
return JSONResponse(status_code=403, content={"detail": "CORS preflight failed: Unauthorized origin"})
response = await call_next(request)
if allow_origin:
self._add_cors_headers(response, allow_origin)
return response
def _add_cors_headers(self, response, origin):
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Vary"] = "Origin"
def _cors_preflight_response(self, origin):
response = JSONResponse(content={"status": "ok"})
self._add_cors_headers(response, origin)
return response