FastAPI 流式响应核心原理解析(含前端断开感知)

一、核心背景

本文基于 FastAPI/Starlette 流式响应源码,结合实际业务接口(`/run` 接口),详细解析流式响应的工作机制、前端断开连接的感知逻辑,以及 ASGI 协议新旧版本的差异,帮助开发者从底层理解流式响应的实现原理。

核心业务场景:后端通过 `StreamingResponse` 向前端推送 AI 推理结果、实时事件(SSE 协议),需解决「前端断开连接后,后端及时停止推流、释放资源」的问题。

二、核心类解析:StreamingResponse

2.1 类的核心作用

`StreamingResponse` 继承自 FastAPI/Starlette 的 `Response` 基类,专门用于异步流式输出,核心优势:

  • 不一次性将所有数据加载到内存,边生成边推送,降低内存占用;

  • 自动兼容同步/异步迭代器(生成器);

  • 遵循 ASGI 协议,支持所有 ASGI 服务器(如 Uvicorn)。

2.2 核心属性与方法

2.2.1 核心属性

`body_iterator: AsyncContentStream`:存储异步迭代器,所有要推送的数据流均从该迭代器读取,是流式响应的核心。

2.2.2 关键方法

  1. init(初始化方法):接收用户传入的迭代器(同步/异步),自动将同步迭代器通过 `iterate_in_threadpool` 包装成异步迭代器,避免阻塞事件循环;同时初始化状态码、响应头、媒体类型、后台任务。

  2. listen_for_disconnect:监听客户端断开事件,通过循环读取 `receive()` 消息,当收到 `http.disconnect` 时退出循环,用于旧版 ASGI 协议的断开感知。

  3. stream_response:流式发送数据的核心方法,分三步执行:

    1. 发送响应头(告知客户端响应开始);

    2. 异步循环读取 `body_iterator` 中的数据块,统一转为 bytes 后推送(`more_body=True` 表示还有后续数据);

    3. 发送空数据块(`more_body=False`),告知客户端传输完成。

  4. call(ASGI 入口方法):ASGI 服务器(如 Uvicorn)会将 `StreamingResponse` 实例当作函数直接调用,是整个流式响应的入口,负责根据 ASGI 版本选择对应的处理逻辑。

三、关键方法详解:call

3.1 核心作用

`call` 是 Python 魔法方法,实现该方法后,类的实例可以像函数一样被调用(`对象()` 等价于调用 `call`)。

在 FastAPI 中,`call` 是 ASGI 应用的标准入口:Uvicorn 等 ASGI 服务器会直接调用 `StreamingResponse` 实例,传入 `scope`(请求上下文)、`receive`(接收客户端消息)、`send`(向客户端发送消息)三个参数,驱动流式响应的执行。

3.2 核心逻辑

`call` 方法的核心是根据 ASGI 协议版本,选择不同的流式处理逻辑:

  • ASGI ≥ 2.4:简化处理,直接调用 `stream_response` 推流,通过捕获 `OSError` 感知客户端断开;

  • ASGI < 2.4:通过任务组并发运行「推流」和「监听断开」两个协程,实现双任务互锁,避免资源浪费。

四、旧版 ASGI(<2.4):双任务并发逻辑解析

4.1 核心代码

复制代码
with collapse_excgroups():
    async with anyio.create_task_group() as task_group:

        async def wrap(func: typing.Callable[[], typing.Awaitable[None]]) -> None:
            await func()
            task_group.cancel_scope.cancel()  # 完成后取消其他任务

        # 任务1:流式发送数据(后台运行)
        task_group.start_soon(wrap, partial(self.stream_response, send))
        # 任务2:监听客户端断开(阻塞等待)
        await wrap(partial(self.listen_for_disconnect, receive))

4.2 核心原理

旧版 ASGI 协议的缺陷:客户端断开连接后,`send()` 方法不会报错,后端无法主动感知,会继续无效推流,因此需要通过「双任务并发」解决该问题。

  1. 任务组(anyio.create_task_group):用于管理多个异步任务的生命周期,一个任务结束/报错,所有任务会被自动取消。

  2. wrap 包装函数:灵魂逻辑,接收一个异步函数,执行该函数后,立即调用 `task_group.cancel_scope.cancel()`,取消整个任务组的所有任务------实现「任一任务结束,立即终止另一任务」。

  3. 双任务分工

    1. 任务1(`stream_response`):后台异步推流,死循环读取数据并发送;

    2. 任务2(`listen_for_disconnect`):阻塞等待客户端断开,死循环读取 `receive()` 消息,收到 `http.disconnect` 后结束。

4.3 两种运行场景

  1. 正常结束:推流完成(任务1结束)→ 触发 `wrap` 取消任务组 → 任务2(监听)被终止;

  2. 前端断开:任务2(监听)收到断开消息并结束 → 触发 `wrap` 取消任务组 → 任务1(推流)被终止,避免无效推流。

五、新版 ASGI(≥2.4):简化逻辑解析

5.1 核心改进(关键!)

ASGI 2.4 协议新增核心规则:当客户端连接已关闭时,调用 `send()` 方法必须抛出 `OSError`(或其子类)

这一改进彻底解决了旧版的缺陷:无需手动监听断开,`send()` 会自动报错,后端通过捕获异常即可感知客户端断开,因此可以删除复杂的双任务并发逻辑。

5.2 核心代码

复制代码
if spec_version >= (2, 4):
    try:
        await self.stream_response(send)  # 单协程推流
    except OSError:
        raise ClientDisconnect()  # 捕获断开异常,抛出统一异常

5.3 前端断开的完整链路

  1. 前端关闭页面/取消请求 → TCP 连接断开;

  2. 后端继续执行 `stream_response` 中的 `await send(...)`;

  3. ASGI 服务器(Uvicorn)发现连接已断开,主动抛出 `OSError`(如 Broken pipe)

  4. 异常被 `try/except` 捕获,抛出 `ClientDisconnect` 异常;

  5. `stream_response` 方法终止,`async for` 推流循环立即停止;

  6. 异常向上冒泡,被业务代码(`event_generator`)捕获,感知到前端断开。

六、业务代码中的前端断开感知

6.1 核心结论

路由函数(如 `run_agent`)本身无法直接感知前端断开,但内部的流式生成器(`event_generator`)可以通过捕获 `OSError` 或 `ClientDisconnect` 异常,精准识别前端断开事件。

6.2 落地代码示例

复制代码
async def event_generator():
    nonlocal content, run_id
    client_disconnected = False  # 断开标记

    try:
        # AI 推理流式输出,前端断开会触发异常,中断该循环
        async for event in run(agent, run_input):
            # 业务逻辑:处理事件、格式化输出
            yield format_sse_event(event)

    # 捕获前端断开异常
    except (OSError, ClientDisconnect) as e:
        print(f"🚫 客户端主动断开连接: {e}")
        client_disconnected = True

    finally:
        # 根据断开标记,控制后续逻辑(如是否保存数据库、生成推荐)
        if client_disconnected:
            print("ℹ️ 客户端断开,不执行后续收尾操作")
        else:
            print("ℹ️ 正常结束,执行推荐生成和数据库保存")
            # 原有收尾逻辑(推荐问题、保存对话)

6.3 关键注意点

  • 前端断开后,`async for event in run(...)` 会立即停止,AI 推理也会同步终止,避免资源浪费;

  • `finally` 块依然会执行,可通过「断开标记」控制是否执行收尾操作(如保存数据库、生成推荐);

  • `OSError` 由 Uvicorn 主动抛出,是新版 ASGI 感知断开的核心机制,100% 可靠。

七、新旧 ASGI 模式对比

对比维度 旧版 ASGI(<2.4) 新版 ASGI(≥2.4)
断开感知方式 主动监听 `receive()` 消息 `send()` 自动抛出 `OSError`
实现复杂度 高(任务组+双协程+包装函数) 低(单协程+try/except)
核心依赖 任务组、取消作用域 ASGI 服务器(Uvicorn)异常抛出
资源占用 较高(双协程并发) 较低(单协程线性执行)
相关推荐
dweizhao1 小时前
别再用 Figma 画线框图了,Google 这款免费工具直接出 UI 稿
前端
han_2 小时前
JavaScript设计模式(五):装饰者模式实现与应用
前端·javascript·设计模式
ProgramHelpOa2 小时前
Amazon SDE Intern OA 2026 最新复盘|70分钟两题 Medium-Hard
java·前端·javascript
smchaopiao2 小时前
如何用CSS和JS搞定全屏图片展示
前端·javascript·css
酉鬼女又兒2 小时前
零基础快速入门前端CSS Transform 与动画核心知识点及蓝桥杯 Web 应用开发考点解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·css·职场和发展·蓝桥杯·html
山川行2 小时前
Python快速闯关8:内置函数
java·开发语言·前端·笔记·python·学习·visual studio
曲幽2 小时前
FastAPI子应用挂载:别再让root_path坑你一夜
python·nginx·fastapi·web·mount·admin·404·docs·root_path
徐小夕3 小时前
花了一周时间,我们开源了一款PDF编辑SDK,支持在线批注+脱敏
前端·vue.js·github
前端Hardy3 小时前
Qwik 2.0 Beta 来了:不靠 AI,只靠 Resumability,首屏交互快到离谱
前端·javascript·面试
1-1=03 小时前
ExtJS 快速入门—— 面板 详细版
前端·jquery