You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI动态Origin的CORS预检请求失败问题求助

动态CORS中间件处理OPTIONS预检请求失败问题

问题场景

做FastAPI后端时,需要允许预定义Origin列表和数据库里动态加载的Origin发起CORS请求。但前端调用GET接口时,浏览器发的OPTIONS预检请求会因为CORS错误失败,导致实际的GET请求根本没发起。

错误原因

原代码的问题集中在OPTIONS请求处理逻辑和动态Origin验证的时机上:

  • OPTIONS请求分支里直接创建了JSONResponse,但后续针对数据库Origin的验证逻辑没覆盖这个分支,导致数据库中存在的Origin在OPTIONS请求时加不上正确的CORS响应头。
  • 数据库Origin验证通过后,重复调用了await call_next(request),会让请求被处理两次,OPTIONS请求还会被错误地二次处理。
  • 当Origin不在默认列表且数据库验证不通过时,直接抛出HTTPException,但OPTIONS请求应该返回符合CORS规范的响应,而非抛出异常,不然浏览器会判定为CORS错误。

修复后的代码

from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.types import ASGIApp

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")
        # 非浏览器请求(无Origin头)直接跳过CORS处理
        if not origin:
            return await call_next(request)
        
        # 先统一验证Origin是否合法
        is_allowed = False
        if origin in origins:
            is_allowed = True
        else:
            # 提取域名(移除协议和端口)
            domain = origin.replace("https://", "").replace("http://", "")
            # 检查数据库中是否存在该域名(注意这里要用await,因为MongoDB的find_one是异步方法)
            domain_exists = await domain_collection.find_one({"$or": [{"domain.main_domain": domain}, {"domain.sub_domain": domain}]})
            if domain_exists:
                is_allowed = True
        
        # 处理OPTIONS预检请求
        if request.method == "OPTIONS":
            if not is_allowed:
                return JSONResponse(content={"detail": "Origin not authorized"}, status_code=403)
            # OPTIONS请求返回空响应即可,不需要业务内容
            response = Response()
        else:
            if not is_allowed:
                raise HTTPException(status_code=403, detail="Origin not authorized")
            # 非OPTIONS请求正常处理业务逻辑
            response = await call_next(request)
        
        # 合法Origin才添加CORS响应头
        if is_allowed:
            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

app = FastAPI(
    title="API",
    description="API documentation for backend",
    version="1.0.0",
    docs_url=None,
    redoc_url=None
)
app.add_middleware(DynamicCORSMiddleware)

关键修复点

  • 提前统一验证Origin:先完成默认列表和数据库的Origin验证,确保OPTIONS和非OPTIONS请求用同一套规则。
  • 正确处理OPTIONS请求:直接返回空Response,避免多余内容;验证不通过时返回标准403响应,符合浏览器预检预期。
  • 避免重复处理请求:仅在非OPTIONS且Origin合法时调用一次call_next,防止请求被重复执行。
  • 异步方法修正:数据库查询用await,保证异步逻辑正确执行。

内容的提问来源于stack exchange,提问作者Rooban

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:44:55