首页
/ FastAPI 集成 WSGI 应用实战:借助 WSGIMiddleware 将 Flask、Django 等挂载进同一 ASGI 服务

FastAPI 集成 WSGI 应用实战:借助 WSGIMiddleware 将 Flask、Django 等挂载进同一 ASGI 服务

2026-09-07 09:01:35作者:段琳惟

导读:本文围绕 FastAPI 官方文档中的"挂载 WSGI 应用(Flask、Django 等)"主题展开,讲解如何在同一个 FastAPI 应用中通过 a2wsgiWSGIMiddleware 将传统的 WSGI 框架应用"嵌入"进来,实现新旧技术栈共存与平滑过渡。读完你将掌握安装依赖、编写示例、挂载路径以及验证请求分发归属的完整方法,并能理解 fastapi.middleware.wsgi 被弃用背后的工程缘由。

背景:为什么需要把 WSGI 应用挂进 FastAPI

FastAPI 基于 ASGI(异步服务器网关接口)规范构建,天然支持异步编程与 WebSocket 等能力。而 Flask、Django 等大量历史项目遵循的是 WSGI(同步的 Web 服务器网关接口)规范。两者协议不同,无法直接相互调用。

但在实际迁移场景中,团队往往希望:

  • 新接口用 FastAPI 开发,享受自动 OpenAPI 文档、数据校验与异步性能;
  • 存量业务仍运行在成熟的 Flask / Django 应用里,不必一次性全部重写
  • 两者运行在同一个进程、同一个端口之下,便于统一部署、统一入口。

FastAPI 官方给出的答案与"子应用挂载(Mounts)"思路一致——把 WSGI 应用当作一个挂在指定路径前缀下的"独立子应用"。这一点正是 Sous-applications - Montages(子应用与挂载)Être derrière un proxy(反向代理部署) 两篇文档所铺垫的挂载与 root_path 机制的延续(中文版见 docs/zh/docs/advanced/sub-applications.mddocs/zh/docs/advanced/behind-a-proxy.md)。

核心概念:WSGIMiddleware 充当 ASGI↔WSGI 的翻译层

要让 FastAPI(ASGI 侧)能把请求交给 Flask(WSGI 侧),需要一层适配中间件来做协议转换:把 ASGI 的 scope / receive / send 调用翻译为 WSGI 的 environ / start_response,再把 WSGI 应用产生的响应翻译回 ASGI。这就是 WSGIMiddleware 的职责。

在当前的 FastAPI 仓库中,挂载 WSGI 应用的推荐做法是引入第三方包 a2wsgi,并从其中导入 WSGIMiddleware

from a2wsgi import WSGIMiddleware

官方文档(本仓库 docs/fr/docs/advanced/wsgi.md,英文源版 docs/en/docs/advanced/wsgi.md)给出的使用路径非常简洁,只有三步:

  1. WSGIMiddleware 包装你的 WSGI 应用(例如 Flask app);
  2. 通过 app.mount(path, ...) 把它挂载到某个路径前缀之下;
  3. 之后该路径前缀下的所有请求都由被挂载的 WSGI 应用处理,其余请求仍由 FastAPI 处理。

第一步:添加依赖

由于 WSGIMiddleware 现在来自第三方包 a2wsgi,使用前需要先安装。官方文档推荐使用 uv

uv add a2wsgi

如果你还需要运行本文的完整示例(它包含一个最小 Flask 应用),同时要安装 Flask:

uv add flask

在仓库自身的工程配置中也能看到这一事实:pyproject.tomltests 依赖组同时纳入了 a2wsgiflask,其中 a2wsgi 版本约束为 >=1.9.0,<=2.0.0flask 版本约束为 >=3.0.0,<4.0.0(参见 pyproject.tomlpyproject.toml),这说明仓库自己的教程测试正是围绕这两个包运行的。

第二步:编写完整示例

仓库在 docs_src/wsgi/tutorial001_py310.py 提供了与官方文档一一对应的可运行示例,全文如下:

from a2wsgi import WSGIMiddleware
from fastapi import FastAPI
from flask import Flask, request
from markupsafe import escape

flask_app = Flask(__name__)


@flask_app.route("/")
def flask_main():
    name = request.args.get("name", "World")
    return f"Hello, {escape(name)} from Flask!"


app = FastAPI()


@app.get("/v2")
def read_main():
    return {"message": "Hello World"}


app.mount("/v1", WSGIMiddleware(flask_app))

逐段拆解这段代码:

  • 第 1~4 行:导入三组能力——a2wsgi 提供的 WSGIMiddleware(协议桥接)、FastAPI 的 FastAPI 类(ASGI 主应用),以及 Flask 与其 request/escape(被挂载的 WSGI 应用)。
  • 第 6~12 行:创建并配置 Flask 应用。它的根路由 / 会读取查询参数 name(缺省为 "World"),并用 markupsafe.escape 转义后拼接进响应文本,防止 XSS。这个 Flask 应用完全是一个标准的、独立可运行的 WSGI 应用。
  • 第 15 行:创建 FastAPI 主应用 app
  • 第 18~20 行:在主应用里定义一条路径操作 GET /v2,作为"FastAPI 侧"的对照接口,返回 JSON {"message": "Hello World"}
  • 第 23 行:关键的挂载语句 app.mount("/v1", WSGIMiddleware(flask_app))——先把 Flask 应用用 WSGIMiddleware 包起来,再挂到 /v1 前缀下。

注意:Flask 侧的 flask_main 路由是 /,但因为它整体被挂载在 /v1 之下,所以实际对外生效的完整路径是 /v1/。也就是说,子应用的内部路由始终是相对于挂载点解析的,这与挂载另一个 FastAPI 子应用时的规则一致。

第三步:理解挂载点的请求分发规则

挂载完成后,路由归属非常简单清晰:

  • 凡是以 /v1/ 开头的请求,全部交给 Flask 应用处理;
  • 其余请求继续由 FastAPI 主应用的路由表处理(本例即 /v2)。

这种"路径前缀 + 独立子应用"的架构,本质上复用了 FastAPI 文档中"子应用挂载"的同一套机制:主应用只负责把前缀匹配到的请求"转发"出去,子应用内部再按自己的路由规则继续分发。如果你之前已经掌握 Sous-applications - Montages(中文版见 docs/zh/docs/advanced/sub-applications.md),那么 WSGI 挂载的唯一新增点就是多套了一层 WSGIMiddleware 协议适配,其余心智模型完全一致。

需要留意的工程细节是:挂载点本身(/v1)并不参与子应用内部的 URL 构造。若 WSGI 应用内部需要知道自己的对外前缀(例如生成链接、处理重定向),通常依赖部署层传递的环境信息;当 FastAPI 自身又位于 Nginx / Traefik 等反向代理之后时,则要与 Derrière un proxy(代理部署) 一文中的 root_path、转发请求头等机制配合处理(中文版见 docs/zh/docs/advanced/behind-a-proxy.md)。

弃用说明:从 fastapi.middleware.wsgi 迁移到 a2wsgi

官方文档特别给出了一条迁移提示:早期版本推荐从 fastapi.middleware.wsgi 导入 WSGIMiddleware,但现在该模块已被弃用,建议改用独立的 a2wsgi 包,使用方式完全一致。

从当前仓库源码可以印证这一点:仓库内的 fastapi/middleware/wsgi.py 只剩下一行再导出代码,即把 Starlette 的 WSGIMiddleware 原样转出(标注了 pragma: no cover),本身不再承载任何 FastAPI 特有的实现逻辑:

from starlette.middleware.wsgi import (
    WSGIMiddleware as WSGIMiddleware,
)  # pragma: no cover # noqa

因此官方将协议适配的实现职责整体移交给了第三方 a2wsgi 维护。从源码结构可以推断,这一调整是为了让 FastAPI 自身的 middleware 包更聚焦于 ASGI 世界,而 WSGI 桥接这类兼容需求交给专门维护的独立库持续跟进。

迁移时你只需要做两件事:

  1. 确保 a2wsgi 已安装(uv add a2wsgi);

  2. 把导入语句从:

    from fastapi.middleware.wsgi import WSGIMiddleware
    

    改为:

    from a2wsgi import WSGIMiddleware
    

其余代码(包装、挂载、请求分发)一概不变。

第四步:运行并验证

启动服务(可通过 FastAPI CLI 或任意 ASGI 服务器运行上面的示例):

uv run fastapi dev docs_src/wsgi/tutorial001_py310.py

然后分别验证两个端点,请求会按预期被不同的框架处理:

访问 http://localhost:8000/v1/ —— 请求前缀命中 /v1/,由 Flask 应用响应:

Hello, World from Flask!

(若携带查询参数,例如 /v1/?name=FastAPI,则会得到经过转义的 Hello, FastAPI from Flask!。)

访问 http://localhost:8000/v2 —— 未命中挂载前缀,交给 FastAPI 主应用响应:

{
    "message": "Hello World"
}

仓库通过测试用例固定了上述行为,见 tests/test_tutorial/test_wsgi/test_tutorial001.py

from fastapi.testclient import TestClient

from docs_src.wsgi.tutorial001_py310 import app

client = TestClient(app)


def test_flask():
    response = client.get("/v1/")
    assert response.status_code == 200, response.text
    assert response.text == "Hello, World from Flask!"


def test_app():
    response = client.get("/v2")
    assert response.status_code == 200, response.text
    assert response.json() == {"message": "Hello World"}

这个测试同时是很好的"冒烟验证"手段:即便没有真实启动服务器,也可以用 FastAPI 自带的 TestClient 断言 WSGI 子应用与 ASGI 主应用各自响应正确、互不干扰。

扩展到 Django 及其他 WSGI 框架

官方文档明确指出该方案不仅限于 Flask,同样适用于 Django 及其他任何遵循 WSGI 规范的框架。模式是通用的:

from a2wsgi import WSGIMiddleware
from fastapi import FastAPI

# django_app 例如来自 django.core.wsgi 的 get_wsgi_application()
app = FastAPI()
app.mount("/django", WSGIMiddleware(django_app))

不过需要说明:Django 这类"全家桶"框架本身包含自己的 URL 路由、ORM 中间件、静态文件与模板体系,挂载后它将作为一个整体服务 /django/ 前缀下的请求,框架内部机制不会与 FastAPI 的依赖注入、请求/响应对象打通——两者只是在传输层共享同一个进程与端口。对于更细粒度的"让 Flask 的某个蓝图或 Django 的某个 app 暴露成 FastAPI 路由"这类诉求,则属于另一套话题,不能与"整体挂载"混为一谈。

小结

登录后查看全文
热门项目推荐
相关项目推荐