FastAPI 自托管文档静态资源:为 Swagger UI 与 ReDoc 配置自定义 CDN 及完全离线部署指南
FastAPI 的自动交互式 API 文档(Swagger UI 与 ReDoc)默认依赖公共 CDN 加载 JavaScript/CSS 资源,这在地域性访问受限、离线环境或内网部署时会造成文档无法显示。本文以 docs/fr/docs/how-to/custom-docs-ui-assets.md 为骨架,结合仓库内 fastapi/openapi/docs.py 的实现源码与配套测试,讲解两种完整的自定义方案:将文档静态资源切换到自选 CDN,以及把 JS/CSS 下载到本地并由 FastAPI 自行托管,实现断网也能查看和调试 API 文档。读完本文你将能独立改造任意 FastAPI 项目的文档路由。
背景:FastAPI 文档 UI 的资源加载机制
FastAPI 的 API 文档包含两套界面:Swagger UI(默认挂在 /docs)和 ReDoc(默认挂在 /redoc)。二者本质上都是「一段 HTML 页面 + 若干 JS/CSS 静态资源」的组合:
- Swagger UI 需要
swagger-ui-bundle.js与swagger-ui.css; - ReDoc 需要
redoc.standalone.js。
默认情况下,这些资源并不随 FastAPI 仓库分发,而是由 FastAPI 生成的 HTML 页面从 CDN(内容分发网络)动态加载。在源码 fastapi/openapi/docs.py 中可以直接看到这些默认 CDN 地址,例如 get_swagger_ui_html() 的 swagger_js_url 参数默认值为 https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js(docs.py L79),get_redoc_html() 的 redoc_js_url 默认加载 https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js(docs.py L236)。
依赖 CDN 会带来两个现实问题:
- 某些网络环境下部分 CDN 域名不可达(例如所在地区屏蔽特定 URL);
- 离线环境或内网部署时完全没有公网访问。
好消息是 FastAPI 将生成文档页 HTML 的逻辑封装成了公开函数,并允许在创建 FastAPI() 实例时关闭自动文档路由。下面的两个方案分别解决上述两种场景。
方案一:将文档资源切换到自定义 CDN
适用场景:网络可达,但你想使用另一个 CDN(例如 https://unpkg.com/),因为默认 CDN 在你所处的地区不可达,或出于版本与来源管控需要。
第 1 步:关闭自动文档路由
FastAPI() 构造函数的 docs_url 与 redoc_url 两个参数控制自动文档页的挂载地址,其默认值分别是 /docs 与 /redoc(见 applications.py L421、L445)。将它们设为 None 即可关闭自动文档:
from fastapi import FastAPI
app = FastAPI(docs_url=None, redoc_url=None)
关闭后 /docs、/redoc 就不会再被 FastAPI 自动注册,这样它也就不会再向页面注入默认 CDN 地址。
第 2 步:手写自定义文档路由
接下来由我们自己注册 /docs 与 /redoc 路由,复用 FastAPI 内部用于生成文档 HTML 的函数,并把 JS/CSS 的 URL 指向新的 CDN。核心函数从 fastapi.openapi.docs 导入:
get_swagger_ui_html(...):生成 Swagger UI 页面;get_redoc_html(...):生成 ReDoc 页面;get_swagger_ui_oauth2_redirect_html():生成 OAuth2 回调辅助页。
以 get_swagger_ui_html() 为例,它接收的关键参数(与文档一一对应,亦可对照 docs.py L40-L111 的签名与默认值):
| 参数 | 含义 | 取值建议 |
|---|---|---|
openapi_url |
文档页拉取 OpenAPI schema 的 URL | 使用 app.openapi_url(默认 /openapi.json) |
title |
显示在浏览器标签页的标题 | 常用 app.title + " - Swagger UI" |
oauth2_redirect_url |
OAuth2 回调辅助页地址 | 使用 app.swagger_ui_oauth2_redirect_url(默认 /docs/oauth2-redirect,见 applications.py L458) |
swagger_js_url |
加载 Swagger UI JavaScript 的 URL | 这里填入自定义 CDN 地址 |
swagger_css_url |
加载 Swagger UI CSS 的 URL | 这里填入自定义 CDN 地址 |
get_redoc_html() 的参数类似,唯一的资源参数是 redoc_js_url。注意这些参数都可以不传——它们带有默认 CDN 值;正是因为我们要覆盖默认值,才需要显式传入。
第 3 步:处理 OAuth2 重定向
提示:
swagger_ui_redirect路径是使用 OAuth2 时的「助手」。如果你的 API 集成了 OAuth2 提供方,用户通过「Authorize」完成认证后,Swagger UI 需要回到一个固定页面来接收携带的授权凭据,再继续与 API 交互——这一过程由 Swagger UI 在后台完成,但前提是存在这个 redirect 回调页。因此在自定义路由时,也需要像下面的代码一样补上get_swagger_ui_oauth2_redirect_html()对应的路由。
完整可运行示例
仓库中的完整示例见 docs_src/custom_docs_ui/tutorial001_py310.py:
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
app = FastAPI(docs_url=None, redoc_url=None)
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js",
swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="https://unpkg.com/redoc@2/bundles/redoc.standalone.js",
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}
关于上述代码还有两个细节值得说明:
include_in_schema=False:FastAPI 在自动注册文档路由时同样使用了该标记(见 applications.py L1137),它保证/docs、/redoc这些"元页面"不会出现在 OpenAPI schema 自身的paths中;- 路由路径直接写
app.swagger_ui_oauth2_redirect_url(值为/docs/oauth2-redirect),与默认行为保持一致。
验证效果
启动应用(例如 uvicorn main:app --reload),访问 http://127.0.0.1:8000/docs 并刷新页面。如果 Swagger UI 正常渲染,说明页面已从新的 unpkg.com CDN 拉取资源。仓库配套测试 tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py 也验证了这一点:它断言 /docs 返回的 HTML 中确实包含 https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js 与对应的 CSS 地址,并检查 /redoc 页包含 unpkg.com 的 ReDoc JS 地址,同时 /docs/oauth2-redirect 与业务接口 /users/john 均返回 200。
方案二:自托管静态资源,实现完全离线可用
适用场景:应用需要在没有公网(离线、断网)或只有内网的环境下持续运行,文档资源必须与应用一起分发、由应用自己提供。
第 1 步:规划项目文件结构
假设项目结构从简如下:
.
├── app
│ ├── __init__.py
│ ├── main.py
先为静态资源创建目录 static/(放在应用目录旁即可):
.
├── app
│ ├── __init__.py
│ ├── main.py
└── static/
第 2 步:下载文档所需的静态文件
把下列文件下载并放入 static/ 目录(在浏览器中右键链接选择"另存为"即可):
Swagger UI 需要:
swagger-ui-bundle.js(来源:https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js)swagger-ui.css(来源:https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css)
ReDoc 需要:
redoc.standalone.js(来源:https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js)
下载完成后目录结构如下:
.
├── app
│ ├── __init__.py
│ ├── main.py
└── static
├── redoc.standalone.js
├── swagger-ui-bundle.js
└── swagger-ui.css
第 3 步:用 StaticFiles 托管静态目录
导入 fastapi.staticfiles.StaticFiles,用 app.mount() 把 static/ 目录挂载到 /static 路径:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
name="static" 相当于给这个挂载点起名,便于后续需要反向解析 URL 时引用。
第 4 步:验证静态文件能被访问
启动应用并访问 http://127.0.0.1:8000/static/redoc.standalone.js,浏览器应返回一个很长的 JS 文件,内容大致以类似下面的许可注释开头:
/*! For license information please see redoc.standalone.js.LICENSE.txt */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):
...
看到这段内容,说明你已经成功用 FastAPI 自身提供静态文件,且文件放置位置正确。接下来只需让文档页面指向这些本地 URL。
第 5 步:关闭自动文档并指向本地资源
与方案一相同,先关闭自动文档;区别在于,这次传给 get_swagger_ui_html() / get_redoc_html() 的不再是外部 CDN 地址,而是 FastAPI 自己服务的相对 URL(/static/...)。
完整示例见 docs_src/custom_docs_ui/tutorial002_py310.py:
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="/static/redoc.standalone.js",
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}
需要注意:ReDoc 页面默认还会额外加载 Google Fonts(get_redoc_html() 的 with_google_fonts 参数默认为 True,见 docs.py L245-L252)。如果连字体资源也要完全离线,可在调用 get_redoc_html() 时传入 with_google_fonts=False,让页面使用系统字体。
第 6 步:断网实测
启动应用后访问 http://127.0.0.1:8000/docs。关闭 Wi-Fi 或断开外网后刷新页面,Swagger UI 依旧能完整加载、正常展示 OpenAPI schema,并可直接对接口发起交互调用——因为页面所需的全部 JS/CSS 都由本应用在 /static 下提供。
配套测试 tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py 验证了同样的结论:它断言 /docs 返回的 HTML 中 swagger_js_url 与 swagger_css_url 已被替换为 /static/swagger-ui-bundle.js、/static/swagger-ui.css,/redoc 页引用 /static/redoc.standalone.js。需要留意的是,该测试的 fixture 会在当前工作目录创建临时 static/ 目录以兼容示例代码的运行前提,因此运行前示例文件 static/ 中的三个静态文件必须真实存在。
源码剖析:FastAPI 究竟如何生成文档页
理解底层实现有助于你在自定义时准确覆盖需要的参数而不踩坑。
fastapi/openapi/docs.py:三个 HTML 生成函数
get_swagger_ui_html()(docs.py L40)返回一个完整的HTMLResponse:它在<head>中写入 CSS 链接,在<body>末尾用<script src="...">引入 JS,随后内联一段SwaggerUIBundle({...})初始化脚本,把openapi_url作为url注入。它默认带有一套swagger_ui_default_parameters(如deepLinking: True、layout: "BaseLayout"等,见 docs.py L22-L37),并支持init_oauth、swagger_ui_parameters等进阶参数。get_redoc_html()(docs.py L197)生成<redoc spec-url="{openapi_url}"></redoc>标签加<script src="{redoc_js_url}">的页面。get_swagger_ui_oauth2_redirect_html()(docs.py L301)直接内嵌了从 Swagger UI 项目拷贝来的 OAuth2 回调逻辑。
实现中还有一个值得注意的安全细节:内联 JSON 参数在拼进 <script> 之前会经过 _html_safe_json() 处理(docs.py L9-L19),把 <、>、& 转义为 Unicode 转义序列,防止在 HTML 中注入恶意脚本。
fastapi/applications.py:默认文档路由的注册逻辑
FastAPI 在实例初始化阶段(setup())会按条件注册文档路由(applications.py L1121-L1158):
if self.openapi_url and self.docs_url:
# 注册 get_swagger_ui_html() 生成的页面到 self.docs_url
...
if self.swagger_ui_oauth2_redirect_url:
# 同时注册 oauth2-redirect 回调页
...
if self.openapi_url and self.redoc_url:
# 注册 get_redoc_html() 生成的页面到 self.redoc_url
...
从这段逻辑可以推断并印证几点:
- 把
docs_url=None、redoc_url=None传入构造函数,就会跳过上述分支,从而彻底移除自动文档路由——这正是两个方案中"第一步"的原理所在; - 自动注册的内部实现,恰恰就是调用我们手动复用的
get_swagger_ui_html()/get_redoc_html()/get_swagger_ui_oauth2_redirect_html()三个函数。因此"自定义文档"本质上不是发明新机制,而是接管 FastAPI 的默认注册行为; - 自动注册还额外处理了
root_path(反向代理场景下的路径前缀),会在openapi_url前拼接根路径前缀。如果你的应用部署在子路径/代理之后,手写自定义文档页时可参考该逻辑自行处理。
总结与适用建议
| 场景 | 推荐方案 | 关键改动 |
|---|---|---|
| 默认 CDN 不可达/需指定资源来源 | 自定义 CDN | docs_url=None, redoc_url=None + 手写 /docs、/redoc 路由指向新 CDN |
| 离线/内网/必须完全自包含 | 自托管静态资源 | 额外下载三个静态文件并 app.mount("/static", StaticFiles(...)),路由指向 /static/... |
| 使用 OAuth2 | 两种方案通用 | 务必同时注册 swagger_ui_redirect 回调路由 |
| 追求完全离线(含字体) | 自托管方案 | get_redoc_html(..., with_google_fonts=False) |
两套方案的可运行代码分别位于仓库的 docs_src/custom_docs_ui/tutorial001_py310.py 与 docs_src/custom_docs_ui/tutorial002_py310.py,配套测试见 tests/test_tutorial/test_custom_docs_ui/ 目录,底层 HTML 生成逻辑见 fastapi/openapi/docs.py。你可以直接以这两份示例为模板改造自己的项目:只需把示例中的业务接口换成你的真实接口,把 static/ 目录与挂载路径按需调整即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00