流式输出注意点

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

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

  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
相关推荐
计算机毕业编程指导师1 分钟前
【Python毕设选题推荐】基于Spark的国内主要农作物产量趋势分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·python·数据分析·spark·毕业设计·课程设计·农作物产量
计算机毕业编程指导师7 分钟前
【Python毕设选题推荐】基于Spark的宫颈癌变光谱特征数据分析可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·计算机·spark·毕业设计·宫颈癌变
ss2738 分钟前
AI全栈实战 | 3.3-01 Python OOP:__init__ 真的是构造函数吗,元类怎么让 Django Model 变魔法
开发语言·python·django
FYKJ_201010 分钟前
springboot心理健康管理系统86254-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·mysql·django·课程设计
海绵宝宝转agent9 小时前
learn-claude-code第1-5章开源学习笔记分享
人工智能·笔记·python·学习
小奇不哭9 小时前
ROS2 单节点,RGB-D 深度相机局部路径提取 + B 样条平滑路径 + YOLOv8 障碍物检测 + 深度测距,用于移动机器人视觉局部循迹。
python·yolov8·ros2·cv2·路径探索循迹
小小龙学IT9 小时前
Python scikit-learn 机器学习库深度解析
python·机器学习·scikit-learn
2601_9620715710 小时前
类变量和全局变量的查找路径有什么区别?
开发语言·python
卷无止境11 小时前
独立开发者的"富矿地带":哪些垂直领域值得你押注一辈子?
后端·python
Java后端的Ai之路11 小时前
Python进阶探索29_eval内置函数
开发语言·python·探索·eval·内置函数