流式输出注意点

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

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

  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
相关推荐
皮卡丘不断更4 小时前
手搓本地 RAG:我用 Python 和 Spring Boot 给 AI 装上了“实时代码监控”
人工智能·spring boot·python·ai编程
爱打代码的小林4 小时前
基于 MediaPipe 实现实时面部关键点检测
python·opencv·计算机视觉
极客小云4 小时前
【ComfyUI API 自动化利器:comfyui_xy Python 库使用详解】
网络·python·自动化·comfyui
闲人编程5 小时前
Elasticsearch搜索引擎集成指南
python·elasticsearch·搜索引擎·jenkins·索引·副本·分片
痴儿哈哈5 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
花酒锄作田5 小时前
SQLAlchemy中使用UPSERT
python·sqlalchemy
SoleMotive.5 小时前
一个准程序员的健身日志:用算法调试我的增肌计划
python·程序员·健身·职业转型
光影少年5 小时前
AI 前端 / 高级前端
前端·人工智能·状态模式
亓才孓5 小时前
[Properties]写配置文件前,必须初始化Properties(引用变量没执行有效对象,调用方法会报空指针错误)
开发语言·python
智商偏低5 小时前
PostGIS+GeoServer+OpenLayers 数据加载无显示问题排查及自定义坐标系配置文档
状态模式