流式输出注意点

最近在使用流式输出进行开发,总结出来以下几个经验

整体主要是关于以下这几点:

  1. 客户端断开 ≠ 服务端自动停止
  2. 必须感知 client disconnect
  3. 阻塞 / CPU 任务不能直接放在流里
  4. 生成器要能被安全 cancel
  5. 异常不能直接 raise
  6. 连接数 ≠ 并发任务数
  7. SSE 一定要禁用代理缓存

以下是具体会碰到的问题:

1. 客户端关闭,服务端还在跑

此处是前端用户已经关闭了,但是后端仍然还给前端在发送消息,出现这个问题的最主要原因是没有检测client disconnect,FastAPI的流式请求用户断开后不会自动给断开,需要通过FastAPI包中的Request包中的is_disconnected方法来进行监测,此处是使用异步的方式来进行监测

复制代码
import asycio
from fastapi import Request

@app.get("/sse")
async def sse(request: Request):

    async def event_generator():
        try:
            while True:
                # 判断request是否还在连接 检测时需要再前面添加await
                if await request.is_disconnected():
                    print("client disconnected")
                    break
        
                yield "data: processing\n\n"
                await asyncio.sleep(1)
        except asycio.CancelledError:
            # 生成器取消后安全关闭其他资源操作
            error_response = {"message": "任务被取消"}
            yield f"data: {error_response}\n\n"
    return StreamingResponse(
        event_generator(request),
        media_type="text/event-stream"
    )

2. 后台任务不要放在流式响应中执行

不要将耗时的后台任务放在流式响应中进行执行,流式响应只适合做实时响应,这样会导致任务执行一直卡死在SSE并且无返回,并且在执行流式响应时需要设置抖动(尤其针对于轮询/进度条的流式响应),防止一直在执行流式响应导致系统卡死

针对于后台任务可以采用放到celery的队列中,队列一般采用redis,如果是比较简单的任务可以采用anyio/asyncio(内存执行)的方式,然后再通过轮询SSE的方式来检查任务执行状态

3. SSE中不要抛出异常

在SSE中不要抛出异常,抛出异常会导致SSE连接直接终端,即使是错误也要通过流式的方式返回后然后再手动停止并回收相关资源,否则前端直接终端后也无法显示错误信息

复制代码
try:
    ...
except Exception as e:
    # 将错误信息可以返回给前端
    yield f"event: error\ndata: {str(e)}\n\n"

4. SSE Header一定要设计完整

需要设置好准确的SSE Header,否则会导致:

  • nginx缓冲
  • 前端等待很久才收到消息

|-----------|---------------------------|
| 问题 | 对应 Header |
| 数据被缓存 | Cache-Control: no-cache |
| 连接被中间层断掉 | Connection: keep-alive |
| 数据被反向代理缓冲 | X-Accel-Buffering: no |

复制代码
# SSE的header模板
headers = {
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "X-Accel-Buffering": "no",  # nginx
}

Cache-Control: no-cache :是为了防止HTTP缓存,但是如果HTTP进行缓存后浏览器直接使用旧响应,也就是再缓存过期之前新传回的SSE数据不再生效,这个部分的设置是为了告诉前端这个响应是实时的,禁止缓存,必须实时传输

Connection: keep-alive :是告诉middleware这个连接时长连接

X-Accel-Buffering: no :这个时告诉nginx不要进行缓存,每一段都需要立刻转发

5. SSE的返回格式

最好是将SSE的返回格式封装成一个函数,参考如下:

复制代码
@dataclass
class _ResponseBody:
    """
    统一响应体结构
    """
    code: int = 0
    message: str = ""
    data: Any = None
    language: str | None = None

    def to_dict(self) -> dict:
        """转换为字典,保留 None 值"""
        return {
            "code": self.code,
            "errorMsg": self.message,
            "data": self.data,
            "language": self.language
        }

def sse_response(*, data: Any = None, code: int = 0, language: str = None, message: str = "") -> str:
    """
    将内容包装为 SSE (Server-Sent Events) 响应
    """
    body = _ResponseBody(code=code, message=message, data=data, language=language)
    return wrap_sse_data(body.to_dict())


def wrap_sse_data(content: str | dict | list | int | float | bool | None) -> str:
    """
    将内容包装为 SSE (Server-Sent Events) 格式
    支持 str, dict, list 及其他 JSON 可序列化类型
    """
    if not isinstance(content, str):
        # 序列化任何非字符串类型并确保是 utf-8 字符串
        content = orjson_dumps_bytes(content).decode("utf-8")
    # SSE返回格式
    return f"data: {content}\n\n"

def orjson_dumps_bytes(obj: Any, *, default: Any = None, option: int | None = None) -> bytes:
    """
    高性能 JSON 序列化(直接返回 bytes)。

    最佳场景:
    - HTTP Response (Starlette/FastAPI)
    - 写入 Redis/消息队列
    - 存入文件
    """
    handler = default if default is not None else _enhanced_default_handler
    final_option = DEFAULT_ORJSON_OPTIONS | option if option is not None else DEFAULT_ORJSON_OPTIONS

    try:
        return orjson.dumps(obj, default=handler, option=final_option)
    except Exception as e:
        # 包装异常,提供更清晰的上下文
        raise ValueError(f"JSON Serialization Failed: {str(e)} - Type: {type(obj)}") from e
相关推荐
未定义.2212 小时前
第3篇:UI自动化核心操作:输入、点击、弹窗、下拉框全场景实战
运维·python·ui·自动化·jenkins·集成测试·pytest
27669582922 小时前
vercel 安全检测逆向 x-vercel-challenge-solution
开发语言·python·solution·vercel-solution·x-vercel·vercel逆向·ensun
dagouaofei2 小时前
AI PPT 工具怎么选?5个维度对比6款产品
人工智能·python·powerpoint
深蓝电商API2 小时前
Scrapy日志系统详解与生产环境配置
爬虫·python·scrapy
Irene.ll2 小时前
DAY25 异常处理
python
努力学习的小洋2 小时前
Python训练打卡Day4:缺失值处理
开发语言·python
郝学胜-神的一滴2 小时前
Python类属性与实例属性详解及MRO算法演进
开发语言·python·程序人生·算法
AI视觉网奇2 小时前
audio2face 实时驱动 2026笔记
开发语言·python
heda32 小时前
zip在linux上解压出错Unicode编码-解决
linux·运维·python