介绍
✅ 什么是 Catch-All 路由?
Catch-All 路由 指的是:一个能匹配“任意路径”的通配型路由。
它一般会使用 路径参数 path 类型,比如:
@app.get("/{full_path:path}")
async def fallback_handler(full_path: str):
return {"msg": f"You visited: {full_path}"}
这个 / 开头的 /{full_path:path},意思是:
• 匹配任何以 / 开头的请求;
• 把完整的路径作为字符串传入 full_path;
• 哪怕路径是 /foo/bar/hello.jpg,它也能匹配!
所以叫 Catch-All,就像“最后一个网兜”,你没被其他精确路径命中,就会掉进去。
URL 路由匹配机制的一部分,和代码有没有 try-catch 是两码事。
部分 | 含义 |
---|---|
/ | 所有路径的开头 |
{full_path} | 这是一个路径参数,名字叫 full_path |
:path | 是这个参数的“类型转换器” |
🔍 :path 是什么意思?
类型 | 描述 |
---|---|
str | 默认,只匹配不含斜杠 / 的一段 |
int | 匹配整数 |
float | 匹配浮点数 |
path | ✅ 匹配多段路径(包括斜杠 /) |
就意味着这个路由会匹配所有请求路径,无论路径有多少层!
也正因如此,它常被叫做 Catch-All(兜底)路由,用于前端 SPA 项目的前端路由支持。
@app.get("/api/user")
def user(): ...
@app.get("/api/task")
def task(): ...
@app.get("/{path:path}")
def fallback(path): ...
访问 /api/user 会进入 user(),访问 /api/task 会进入 task()。
但如果你访问:
• /api/does-not-exist → ❗ 不会 404!
• 会进入 fallback(path),path == “api/does-not-exist”
这就叫 Catch-All。
路由顺序
-
它是匹配所有路径的,如果注册早了,就会“截胡”所有后面的路由。
-
FastAPI 按照注册顺序来判断匹配优先级,谁先注册,谁优先。
-
如果 Catch-All 早注册,那就没有“后面”了,你的 /api/v1/** 根本用不上!
🎯 问题核心:setup_admin(app) 是个「兜底路由(catch-all)」
SQLAdmin 或类似的后台管理插件,一般都会注册一个这样的路由:
@app.get("/{full_path:path}")
async def catch_all_route(full_path: str): ...
🚨 所有未被上面 include_router 明确匹配到的路径请求,都会被这个 catch-all 捕获处理。
• 本来这个应该走 user_router 的 CORS 预检流程;
• 但是如果这个路由在 setup_admin(app) 之后注册了,那么 catch-all 路由先注册,它就优先匹配了!
• 而 catch-all 路由并不会处理跨域(CORS OPTIONS 请求);
• 所以浏览器就报了 CORS 跨域错误 ⚠️
# 1. 注册所有业务接口
app.include_router(api_router1)
app.include_router(api_router2)
# 2. 注册 SQLAdmin(会用 catch-all)
setup_admin(app)
# 3. 注册前端静态资源路由(if any)
app.include_router(web_router.router)
路由找不到报CORS?
FastAPI 的中间件执行顺序 + catch-all 路由的作用范围 + CORS 的工作机制
app.add_middleware(
CORSMiddleware, # 跨域处理
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"], # 允许所有方法
allow_headers=["*"], # 允许所有头部
)
✅ 你已经注册了 CORSMiddleware,那为啥还报跨域?
上述代码:
所有经过 FastAPI 路由匹配成功的请求(包括 OPTIONS 预检请求)都会自动添加 Access-Control-Allow-* 相关响应头从而避免跨域错误!
❌ 但为什么“未命中的路由”还会报跨域?
这是核心重点:
🔥CORS 中间件只作用于:能被 FastAPI 接收到的请求
⚠️ 如果这个 Catch-All 路由没有处理 CORS,结果就是:
-
浏览器先发出 预检请求 OPTIONS /api/v1/xxx;
-
FastAPI 查找路由匹配不到(因为这个接口没注册或被覆盖);
-
匹配到你的 Catch-All;
-
但 Catch-All 没有注册 OPTIONS 方法;
-
FastAPI 返回 405 或 404;
-
CORSMiddleware 不生效(它只对合法路由生效);
-
浏览器就 报跨域错误。
✅ 这一段已经告诉 FastAPI:我允许跨域请求,包括 OPTIONS 方法。
但!!!问题的关键不是你有没有加 allow_methods=[“*”],而是:
❗ 请求路径 /some/path 根本没注册任何路由!也就没有任何 handler 能接住 OPTIONS 请求!
你要知道:
FastAPI 的 CORS 中间件只会对 存在的路由 生效
❌ 所以:如果你的某个路径根本没被 app.include_router() 注册
比如请求的是 /aichat/pub_agent/expend/dict/,但你没有注册它,那么:
• FastAPI 连 OPTIONS 都不知道该交给谁处理
• 它直接走到了 404 或 405
• 然后 CORS middleware 根本不会处理它
• ✅ 最终结果:浏览器就看到一个没有 CORS headers 的响应,报 CORS error
[请求进来]
↓
中间件先处理 (CORSMiddleware 会拦截 OPTIONS 请求)
↓
如果请求路径存在 → CORSMiddleware 会自动响应 OPTIONS 并添加 CORS headers ✅
↓
如果请求路径不存在 → 中间件不处理,FastAPI 抛出 404 ❌
✔ 方法一:确认你的 catch-all 路由支持 OPTIONS 请求
@router.api_route("/{full_path:path}", methods=["GET", "POST", "OPTIONS"])
async def catch_all(full_path: str, request: Request):
...
⚠️ 即使你什么都不做,也得让它 return 一个空的响应,这样 CORSMiddleware 就会处理它。
✔ 方法二:手动添加一个兜底的 OPTIONS 响应
from fastapi.responses import Response
@router.options("/{full_path:path}")
async def options_handler(full_path: str):
return Response()
原因 | 说明 |
---|---|
CORS 中间件只对匹配成功的路由生效 | OPTIONS 预检必须也要命中 |
Catch-All 如果不支持 OPTIONS,就会报错 | 浏览器就会认为是跨域失败 |
路由顺序、注册方式影响是否生效 | 后注册的 Catch-All 可能覆盖其他逻辑 |