FastAPI访问/docs接口文档显示空白、js/css无法加载
如图:
原因是FastAPI的接口文档默认使用https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css
和https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js
来渲染页面,而这两个URL是外网的CDN,在国内响应超慢,导致请求超时了。
## How to fix:
**NOTE**: 有直达外网VPN的话,直接开启VPN就可以,一行代码都不用改。
官方文档给出的解决方案是定制文档响应函数Custom Docs UI Static Assets (Self-Hosting) - FastAPI
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@next/bundles/redoc.standalone.js", ) @app.get("/users/{username}") async def read_user(username: str): return {"message": f"Hello {username}"}
这虽然能解决问题,但会导致每次创建新项目,都需要拷贝这些相似的代码,不够pythonic
## More efficient
花了好几天的时间,写了一个开源库fastapi-cdn-host,把它变沉一行代码就能搞定的:
# pip install fastapi_cdn_host import fastapi_cdn_host from fastapi import FastAPI app = FastAPI() # 启动app时会自动将CDN更换为响应速度较快的那个 fastapi_cdn_host.patch_docs(app)
注:如果是要支持离线文档,只需在main.py的同级目录下,放置static文件夹(里面需包含swagger-ui-bundle.js和swagger-ui.css),启动时就会自动挂载并使用它。
BTW:写这个库本身没花多少时间,但是写它的单元测试,真真是花了很多很多时间。由于实际测试的场景也不是很多,难免会有纰漏,欢迎来提issue: https://github.com/waketzheng/fastapi-cdn-host
> 仅支持0.100.0以上版本的fastapi,老版本的请直接用官网示例。
## 2024.05.12 补充:一个详细一点的示例
- main.py
#!/usr/bin/env python import os import sys from contextlib import asynccontextmanager from datetime import datetime import fastapi_cdn_host from fastapi import FastAPI, Request from fastapi.responses import RedirectResponse __version__ = "1.0.0" @asynccontextmanager async def lifespan(app: FastAPI): # 注册数据库、挂载Redis等…… yield app = FastAPI(, lifespan=lifespan, version=__version__) fastapi_cdn_host.patch_docs(app) @app.get("/", include_in_schema=False) async def root(request: Request) -> RedirectResponse: """首页直接跳转到文档""" return RedirectResponse("/docs") @app.get("/robots.txt", include_in_schema=False) async def robots_txt(): """声明不允许爬虫访问""" return """ User-agent: * Disallow: / """ @app.get("/app") async def app_info(request: Request) -> dict[str, str | dict | datetime | None]: """展示客户端IP、服务器时间等信息""" headers = dict(request.headers) ip = getattr(request.client, "host", "") url = request.url return { "your ip": ip, "version": __version__, "now": datetime.now(), "headers": headers, "path": request.url.path, "url": {"scheme": url.scheme, "hostname": url.hostname, "port": url.port}, } def _runserver() -> int: """This is for debug mode to start server. For prod, use supervisor+gunicorn instead.""" return os.system("fastapi dev") if __name__ == "__main__": # pragma: no cover sys.exit(_runserver())