python flask框架 SSE流式返回、跨域、报错

复制代码
from flask import Flask, Blueprint, request, Response, stream_with_context, abort
from flask_cors import CORS
return Response(xxx) 是手动构造响应对象,自由度最高,可以实现流式输出
Response(
response, # 【最重要】响应内容,可以是字符串、迭代器、生成器
content_type, # 响应数据类型
headers # 自定义http响应头
)
"""
"""
search_sse_v1它不是普通函数,是生成器函数(内部有 yield)
stream_with_context(生成器)
作用:保证生成器迭代时,flask请求上下文不会销毁!sse必须加!
Flask 工作机制:
请求进来 → 进入视图函数 search_sse_v1()
视图函数执行到 return Response(...) → 视图函数执行结束
👉 默认:请求上下文(request、current_app 等)直接销毁!
但是!
生成器是延后执行的:视图函数search_sse_v1 return 之后,框架才会慢慢迭代生成器、读取 yield 的数据。
上下文销毁后,生成器里面一旦用到 request/app,直接报错!
stream_with_context(迭代器)
作用:把请求上下文 "保存" 住,迭代生成器的时候上下文依然有效。只要做流式 SSE,必须套这个!
content_type='text/event-stream'
标准SSE响应类型,告诉客户端这是流式长连接
本次返回不是普通 json/html,是 SSE 流式长连接
如果不写这个:
浏览器会当成普通文本一次性接收,不会开启流式解析,SSE 直接失效
常见对比:
application/json:普通接口一次性返回 json
text/html:网页
text/event-stream:SSE 服务端推送流
额外响应头:
Cache-Control: no-cache 禁止浏览器缓存流数据
Connection: keep-alive 保持长连接
SSE 是长连接,不加这两个头,部分浏览器 / 代理会提前切断流。
1. 浏览器POST请求 /v1/search/sse
2. 进入视图函数 search_sse_v1()
3. 执行参数解析、鉴权、各种参数校验
4. 调用 search_sse_v1(参数) → 创建生成器(⚠️ 此时生成器内部代码还没运行!)
5. 用 stream_with_context 包裹生成器
6. 构造 Response 对象 return 返回
👉 【视图函数到此执行完毕!】
===== 分割线 =====
7. Flask框架发现Response内部是一个生成器
8. 框架开始循环:迭代生成器,执行生成器内部代码
9. 生成器 yield 一段sse消息
10. Flask立刻把这段数据通过网络发给浏览器
11. 暂停,继续循环,等待下一次yield
12. 所有yield完成,连接关闭
视图函数代码一次性跑完;生成器代码是 return 之后,框架逐步慢慢执行!
Q1:为什么不能直接 return 生成器?
Flask 不识别直接返回的生成器,会报错。必须套 Response。
Q2:stream_with_context 可以省略吗?
如果你的生成器里面完全不使用 request /current_app/logger,勉强能跑;
一旦需要打印日志、拿请求信息,必崩。开发规范:SSE 一律加上。
Q3:Response 是把所有 yield 数据收集起来再发送?
❌ 不是!
Response 收到生成器之后,迭代一次 yield,就立刻往外推送一次,不会缓存全部内容。这就是流式的本质。
极简总结背诵版
return Response():手动构造 HTTP 响应对象,支持传入生成器实现分段输出;
第一个参数放被 stream_with_context 包裹的生成器;
stream_with_context:保留请求上下文,防止生成器运行时环境失效;
content_type='text/event-stream':标记这是 SSE 流,让客户端启用流式解析;
视图函数先执行完所有参数校验,return Response 之后,框架才逐步执行生成器里面的 yield 逻辑。
python 复制代码
# Response 、 stream_with_context、abort
from flask import Flask, Blueprint, request, Response, stream_with_context, abort
# 跨域
from flask_cors import CORS
import json
import time

# 初始化flask应用
app = Flask(__name__)
CORS(app)
# 注册蓝图,和你代码保持一致
search_sse = Blueprint('search_sse', __name__)


def search_sse_v1(sid):
    """
    ✅ SSE核心:生成器函数,依靠 yield 分段推送数据
    不能一次性return,循环不断yield分片
    """
    # ========== 模拟大模型/搜索流式分片结果 ==========
    answer_chunks = [
        "Flask SSE流式返回讲解开始。",
        "SSE全称Server-Sent Events,服务端单向推送。",
        "依靠 yield 持续输出文本分片。",
        "stream_with_context 用来保留flask请求上下文。",
        "消息格式必须遵循 data:xxx\\n\\n 规范。"
    ]

    # 循环推送每一段内容
    for chunk in answer_chunks:
        resp_data = {
            "sid": sid,
            "content": chunk,
            "finished": False  # 是否结束标记
        }
        # SSE标准格式!!重点
        # data: 内容 + 两个换行 \n\n
        # ensure_ascii=False,不转义中文等非 ASCII 字符,直接输出原始文字
        # ensure_ascii=True,前端收到的是 \uxxxx 转义字符串;
        yield f"data: {json.dumps(resp_data, ensure_ascii=False)}\n\n"
        time.sleep(0.6)  # 模拟接口处理耗时,方便观察流式效果

    # ========== 推送结束消息,通知前端流完成 ==========
    end_data = {
        "sid": sid,
        "content": "",
        "finished": True
    }
    yield f"data: {json.dumps(end_data, ensure_ascii=False)}\n\n"


# ---------------- SSE接口路由 核心代码 ----------------
@search_sse.route('/v1/search/sse', methods=["POST"])
def search_sse_controller():
    try:
        # 1、获取原始请求body 二进制流
        data = request.get_data()
        # 2、解析json参数
        try:
            data = json.loads(data)
        except Exception as e:
            # json解析失败,参数错误
            raise Exception("parameter_error")

        app.logger.info(f"search_sse_v1, data: {data}")
        # ====================== 流式返回核心 ======================

        """
        return Response(xxx) 是手动构造响应对象,自由度最高,可以实现流式输出
        Response(
            response,      # 【最重要】响应内容,可以是字符串、迭代器、生成器
            content_type,  # 响应数据类型
            headers        # 自定义http响应头
        )
        """

        """
            search_sse_v1它不是普通函数,是生成器函数(内部有 yield)
        
            stream_with_context(生成器)
            作用:保证生成器迭代时,flask请求上下文不会销毁!sse必须加!
            Flask 工作机制:
            请求进来 → 进入视图函数 search_sse_v1()
            视图函数执行到 return Response(...) → 视图函数执行结束
            👉 默认:请求上下文(request、current_app 等)直接销毁!
            但是!
            生成器是延后执行的:视图函数search_sse_v1 return 之后,框架才会慢慢迭代生成器、读取 yield 的数据。
            上下文销毁后,生成器里面一旦用到 request/app,直接报错!
            
            stream_with_context(迭代器)
            作用:把请求上下文 "保存" 住,迭代生成器的时候上下文依然有效。只要做流式 SSE,必须套这个!


            content_type='text/event-stream'
            标准SSE响应类型,告诉客户端这是流式长连接
            本次返回不是普通 json/html,是 SSE 流式长连接
            如果不写这个:
            浏览器会当成普通文本一次性接收,不会开启流式解析,SSE 直接失效
            常见对比:
                application/json:普通接口一次性返回 json
                text/html:网页
                text/event-stream:SSE 服务端推送流

            额外响应头:
            Cache-Control: no-cache 禁止浏览器缓存流数据
            Connection: keep-alive 保持长连接
            SSE 是长连接,不加这两个头,部分浏览器 / 代理会提前切断流。
            
            
            1. 浏览器POST请求 /v1/search/sse
            2. 进入视图函数 search_sse_v1()
            3. 执行参数解析、鉴权、各种参数校验
            4. 调用 search_sse_v1(参数) → 创建生成器(⚠️ 此时生成器内部代码还没运行!)
            5. 用 stream_with_context 包裹生成器
            6. 构造 Response 对象 return 返回
            👉 【视图函数到此执行完毕!】
            
            ===== 分割线 =====
            7. Flask框架发现Response内部是一个生成器
            8. 框架开始循环:迭代生成器,执行生成器内部代码
            9. 生成器 yield 一段sse消息
            10. Flask立刻把这段数据通过网络发给浏览器
            11. 暂停,继续循环,等待下一次yield
            12. 所有yield完成,连接关闭
            
            视图函数代码一次性跑完;生成器代码是 return 之后,框架逐步慢慢执行!
            
            Q1:为什么不能直接 return 生成器?
            Flask 不识别直接返回的生成器,会报错。必须套 Response。
            
            Q2:stream_with_context 可以省略吗?
            如果你的生成器里面完全不使用 request /current_app/logger,勉强能跑;
            一旦需要打印日志、拿请求信息,必崩。开发规范:SSE 一律加上。
            
            Q3:Response 是把所有 yield 数据收集起来再发送?
            ❌ 不是!
            Response 收到生成器之后,迭代一次 yield,就立刻往外推送一次,不会缓存全部内容。这就是流式的本质。
            
            
            极简总结背诵版
            return Response():手动构造 HTTP 响应对象,支持传入生成器实现分段输出;
            第一个参数放被 stream_with_context 包裹的生成器;
            stream_with_context:保留请求上下文,防止生成器运行时环境失效;
            content_type='text/event-stream':标记这是 SSE 流,让客户端启用流式解析;
            视图函数先执行完所有参数校验,return Response 之后,框架才逐步执行生成器里面的 yield 逻辑。
            
        """

        return Response(

            response=stream_with_context(search_sse_v1(data.get("sid"))),

            content_type='text/event-stream',
            headers={
                "Cache-Control": "no-cache",  # 禁止浏览器缓存流式数据
                "Connection": "keep-alive"  # 保持TCP长连接,不要传输完立刻断开
            }
        )

    except Exception as e:
        app.logger.error(f"SSE接口异常: {str(e)}")
        # abort() 是 Flask 内置函数,作用:立刻终止当前请求,直接给前端返回指定 HTTP 错误码,后面代码不再执行。
        """
        拆解 abort(500, e.args[0])
        第一个参数 500
        HTTP 500 = 服务器内部错误。
        代表后端代码 / 业务逻辑出现异常。
        第二个参数 e.args[0]
        e:上方 except Exception as e 捕获到的异常对象
        e.args:元组,存储异常提示文字
        e.args[0]:取出异常里自定义的错误文本(比如你抛的 Exception("参数错误:search"))
        整体效果:
        接口报错时,前端收到 HTTP 状态码 500,同时携带错误描述字符串。
        """
        abort(500, e.args[0])


# 注册蓝图到app
app.register_blueprint(search_sse)

if __name__ == '__main__':
    # ✅ threaded=True 必须开启!单线程模式流式会阻塞!
    app.run(host="127.0.0.1", port=5000, debug=True, threaded=True)
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>SSE POST流式测试</title>
</head>
<body>
<h3>流式输出结果:</h3>
<div id="result" style="white-space: pre-wrap;font-size:16px;"></div>

<script>
    async function startSSE() {
        const resultDom = document.getElementById("result");
        // 请求参数,和后端接口要求匹配
        const reqBody = {
            "search": "学习Flask SSE流式接口",
            "search_type": 1,
            "sid": null,
            "hid": null,
            "regenerate": null,
            "tab_no": "all",
            "code": ""
        };

        try {
            const response = await fetch("http://127.0.0.1:5000/v1/search/sse", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify(reqBody)
            });

            // 获取流读取器
            const reader = response.body.getReader();
            const decoder = new TextDecoder("utf-8");
            let buffer = "";

            while (true) {
                const {done, value} = await reader.read();
                if (done) {
                    resultDom.innerHTML += "<br><br>✅ 流式传输全部结束";
                    break;
                }

                // 二进制转字符串,放入缓冲区
                buffer += decoder.decode(value);

                // SSE消息分隔符:两条换行 \n\n
                const chunks = buffer.split("\n\n");
                buffer = chunks.pop(); // 剩余不完整内容留在buffer,下次继续解析

                for (const msg of chunks) {
                    if (!msg.startsWith("data: ")) continue;
                    // 截取data: 后面的json字符串
                    const jsonStr = msg.replace("data: ", "");
                    const data = JSON.parse(jsonStr);
                    console.log("收到分片:", data);
                    resultDom.innerHTML += data.content;

                    if (data.finished) {
                        reader.cancel(); // 主动关闭流
                    }
                }
            }
        } catch (err) {
            resultDom.innerHTML += `<br>❌ 请求异常:${err.message}`;
            console.error(err);
        }
    }

    // 执行
    startSSE();
</script>
</body>
</html>
相关推荐
像风一样的男人@1 小时前
python --fastapi推流AI推理
人工智能·python·fastapi
2601_956319881 小时前
最新量化开发卡住,先查规则和流程是否完整
人工智能·python
柒星栈2 小时前
PHP 源码怎么加密防破解?三套方案实战指南
开发语言·php·android studio
勉灬之2 小时前
Next.js + Prisma 跨平台部署踩坑记
开发语言·javascript·ecmascript
小小代码狗2 小时前
SQLi-Labs 基础注入实战教程(Less-1 ~ Less-5and Less-9)
服务器·python·php
这是个栗子2 小时前
前端开发中的常用工具函数(九)
开发语言·javascript·ecmascript·at
蓝创工坊Blue Foundry2 小时前
图片文字提取到 Excel:批量任务如何先定义要交付的字段
运维·服务器·开发语言·数据库·自动化·ocr·excel
会飞的小新2 小时前
C 标准库之 <fenv.h> 详解与深度解析
c语言·开发语言·microsoft
nanawinona2 小时前
2026年下半年量化学习,不同基础要查不同缺口
人工智能·python