FastAPI 自定义文档 UI 静态资源:换 CDN 与完全自托管 Swagger UI / ReDoc
FastAPI 自动生成的 API 文档(Swagger UI 与 ReDoc)默认从公共 CDN 加载 JavaScript 和 CSS 文件,这意味着文档页面在离线环境、内网环境或 CDN 被限制的部署场景下可能无法使用。本文基于官方文档 docs/de/docs/how-to/custom-docs-ui-assets.md(对应 示例源码 1、示例源码 2),完整讲解两种替代方案:替换为自定义 CDN 的静态资源地址,以及把 JS/CSS 文件下载到本地、由 FastAPI 应用自身托管。读完本篇,你将能够离线运行 FastAPI 的交互式文档,并理解 get_swagger_ui_html 等内部函数是如何拼装文档页面的。
默认行为:文档资源来自 CDN
FastAPI 的 /docs(Swagger UI)与 /redoc(ReDoc)页面本身是服务端动态生成的 HTML,但页面里引用的浏览器端资源(Swagger UI 的 JS 与 CSS、ReDoc 的 JS)默认由 CDN 提供。从源码 fastapi/openapi/docs.py 可以看到默认值:
get_swagger_ui_html()的默认参数:swagger_js_url为 jsDelivr CDN 上的swagger-ui-dist@5/swagger-ui-bundle.js,swagger_css_url为同版本的swagger-ui.css;get_redoc_html()的默认参数:redoc_js_url为 jsDelivr CDN 上的redoc@2/bundles/redoc.standalone.js。
因此只要应用能访问外网 CDN,文档页面开箱即用;而本文要解决的正是“不能用默认 CDN”的场景。
方案一:使用自定义 CDN
假设你想改用另一个 CDN(例如 unpkg.com)。这在某些公共 CDN 域名被限制或不可达的网络环境下特别有用。完整示例见 docs_src/custom_docs_ui/tutorial001_py310.py。
第一步:禁用自动文档
自动生成的 /docs 与 /redoc 路由默认使用默认 CDN,所以第一步是创建 FastAPI 应用时把它们的 URL 设为 None:
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)
从源码结构看,fastapi/applications.py 中 setup() 方法只在 self.openapi_url and self.docs_url 同时成立时才注册 swagger_ui_html 路由(约第 1121 行),redoc_url 同理(约第 1149 行)。把 docs_url / redoc_url 置为 None 后,这两条自动路由就不会被添加,而 openapi_url 保持默认 /openapi.json,OpenAPI Schema 依旧对外提供。
第二步:创建自定义文档的路径操作
可以复用 FastAPI 的内部函数来生成文档 HTML 页面,并传入自己需要的参数:
openapi_url:文档 HTML 页面获取 API OpenAPI Schema 的 URL,可直接用应用属性app.openapi_url;title:API 标题(显示在浏览器标签页);oauth2_redirect_url:OAuth2 重定向地址,可用app.swagger_ui_oauth2_redirect_url使用默认值;swagger_js_url:Swagger UI 页面加载 JavaScript 文件的 URL,这里填自定义 CDN 地址;swagger_css_url:Swagger UI 页面加载 CSS 文件的 URL,这里填自定义 CDN 地址。
ReDoc 的用法类似,只是换用 get_redoc_html 并传 redoc_js_url:
@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",
)
提示:
swagger_ui_redirect这个路径操作是配合 OAuth2 使用的辅助页。如果你的 API 集成了 OAuth2 提供商,文档页面可以发起认证并带着凭据返回,Swagger UI 在幕后完成授权流程,但它需要这个“重定向”辅助页面来接收回调参数。这也是为什么自定义文档路由时通常要一并注册它。
第三步:加一个测试用路径操作
为了验证一切正常,可以加一个最简单的业务接口:
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}
第四步:测试
启动应用后访问 http://127.0.0.1:8000/docs 并刷新页面,此时 Swagger UI 的 JS/CSS 已从新的 CDN 加载。可以用浏览器开发者工具的网络面板确认资源请求发往的是你配置的 CDN 域名。
方案二:完全自托管文档的 JavaScript 和 CSS
如果应用需要离线运行(无外网、纯内网、本地网络),最彻底的办法是把文档所需的全部 JS/CSS 下载下来,在同一个 FastAPI 应用里自己托管。完整示例见 docs_src/custom_docs_ui/tutorial002_py310.py。
项目文件结构
假设项目结构如下:
.
├── app
│ ├── __init__.py
│ ├── main.py
新建一个存放静态文件的目录 static/:
.
├── app
│ ├── __init__.py
│ ├── main.py
└── static/
下载所需文件
把文档需要的静态文件下载并放入 static/ 目录(浏览器中对资源链接“另存为”即可)。Swagger UI 需要两个文件:swagger-ui-bundle.js(swagger-ui-dist@5 版本)与 swagger-ui.css(同版本);ReDoc 需要一个文件:redoc.standalone.js(redoc@2 的 bundles 版本)。下载后结构如下:
.
├── app
│ ├── __init__.py
│ ├── main.py
└── static
├── redoc.standalone.js
├── swagger-ui-bundle.js
└── swagger-ui.css
文件版本应与 fastapi/openapi/docs.py 中默认 CDN 参数一致(swagger-ui-dist@5 与 redoc@2),以保证行为与官方默认体验相同。
托管静态文件
- 导入
StaticFiles(FastAPI 直接从 Starlette 转出,见 fastapi/staticfiles.py); - 把
StaticFiles()实例 “mount” 到指定路径:
from fastapi.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
先验证静态文件可访问
启动应用后访问 http://127.0.0.1:8000/static/redoc.standalone.js,应该能看到一份很长的 ReDoc JavaScript 文件,开头类似:
/*! For license information please see redoc.standalone.js.LICENSE.txt */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):
...
这就说明应用能正确对外提供静态文件,且文件放对了位置。
禁用自动文档并接入本地资源
与方案一相同,创建应用时设置 docs_url=None, redoc_url=None。然后创建自定义文档路径操作,区别在于:swagger_js_url、swagger_css_url、redoc_js_url 现在指向自己应用托管的本地路径(相对根路径):
@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}"}
同样保留 swagger_ui_redirect 路径操作,OAuth2 流程才能完整工作。
离线验证 UI
此时可以断开 Wi-Fi,访问 http://127.0.0.1:8000/docs 并刷新页面——即使完全没有互联网,文档页面依旧可见、可交互。测试用例 tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py 验证了自托管示例中 /docs、/redoc 与静态文件路径都能被正确访问。
源码级细节:这些内部函数做了什么
理解两个生成函数后,自定义 URL 的原理一目了然(见 fastapi/openapi/docs.py):
get_swagger_ui_html()(约第 40 行起)返回一段 HTML 字符串:<link rel="stylesheet" href="{swagger_css_url}">与<script src="{swagger_js_url}">就是你在自定义路由里传入的 URL;页面内嵌的SwaggerUIBundle({ url: '{openapi_url}', ... })负责在浏览器端拉取 OpenAPI Schema 并渲染。此外它还会注入swagger_ui_parameters(默认含dom_id、layout、deepLinking等)以及可选的init_oauth。get_redoc_html()(约第 197 行起)生成的页面更简单:一个<redoc spec-url="{openapi_url}">自定义元素加一个<script src="{redoc_js_url}">。- 注意源码中内嵌 JSON 会经过
_html_safe_json()转义<、>、&(约第 9 行),防止把动态内容注入<script>时产生 HTML 注入问题——这也是官方模板可放心拼入openapi_url等动态值的原因。 - 从源码结构看,自动文档路由(
setup()中)会把请求上下文里的root_path拼到openapi_url与oauth2_redirect_url前面(fastapi/applications.py 约第 1121–1158 行),以支持部署在反向代理子路径下。如果你自定义文档路由且应用带有root_path,可参考这一处理方式,为openapi_url手动加上前缀。 - 默认参数方面,
get_swagger_ui_html还支持swagger_favicon_url(默认指向 fastapi.tiangolo.com 的 favicon)与init_oauth,get_redoc_html支持with_google_fonts(默认开启 Google Fonts)。若你的环境同样无法访问这些外部地址,可一并传入本地或内网地址,让文档页彻底去外部化。
小结
- 两种方案共享同一个前置步骤:
FastAPI(docs_url=None, redoc_url=None)关闭自动文档路由,再用get_swagger_ui_html/get_redoc_html/get_swagger_ui_oauth2_redirect_html自建/docs、/redoc与 OAuth2 重定向路径操作(三个示例路由均带include_in_schema=False,避免文档路由本身出现在 Schema 中)。 - 换 CDN 只需把
swagger_js_url、swagger_css_url、redoc_js_url指向新的 CDN 地址;完全自托管则用StaticFiles挂载本地static/目录,并把这些参数改为本地路径,从而让 API 文档在离线、内网环境中照常工作。 - 参考材料:文档 docs_src/custom_docs_ui/tutorial001_py310.py、docs_src/custom_docs_ui/tutorial002_py310.py,实现 fastapi/openapi/docs.py、fastapi/applications.py,测试 tests/test_tutorial/test_custom_docs_ui/。
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 StartedRust0622
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