asp.net core 6+ 中最稳妥的 sse 实现是手动写入 httpresponse.body 并禁用缓冲:需设 statuscode=200、contenttype="text/event-stream"、cache-control="no-cache"、connection="keep-alive",每次写 data: 后调 flushasync,所有 await 必传 httpcontext.requestaborted,且 sse 消息须用 lf 换行、避免 json 原生换行破坏格式。

ASP.NET Core 6+ 中直接写入 HttpResponse.Body 是最稳的 SSE 方式
别绕弯子——用 IActionResult 包装流(比如 FileStreamResult)在 .NET 6+ 里看似简洁,但实际会触发响应缓冲,导致事件延迟甚至卡死。真正在生产环境跑得稳的,是手动控制 HttpResponse 流并禁用缓冲。
关键不是“能不能”,而是“会不会漏掉这几步”:
-
Response.StatusCode = 200必须显式设,不能依赖默认值 -
Response.ContentType = "text/event-stream"缺少charset=utf-8在某些客户端(如旧版 Safari)可能解析乱码 - 必须加
Response.Headers.Add("Cache-Control", "no-cache")和Response.Headers.Add("Connection", "keep-alive"),否则 Nginx 或 IIS 可能提前关闭连接 - 每次写完一条消息后,必须调用
await Response.Body.FlushAsync();用StreamWriter的话要设AutoFlush = true,否则开发环境下(尤其 IIS Express)大概率卡住 - 所有
await操作(包括FlushAsync、Delay、读取下游 AI API 流)都得传入HttpContext.RequestAborted,否则客户端断连时后台任务还在跑,内存和连接数悄悄涨
写 SSE 消息时,data: 行和空行的格式不能错
SSE 协议对换行极其敏感:浏览器只认
(LF),不认
(CRLF)作为消息分隔;多行 data: 会被自动拼接,但中间若混入未转义的
(比如 JSON 字符串里含换行),就会被当成新消息开头,导致前端 EventSource 解析失败。
正确写法只有两种安全路径:
- 手拼字符串:
await writer.WriteLineAsync($"data: {{"msg":"{text.Replace(" ", "\n")}"}}"); await writer.WriteLineAsync("");—— 注意Replace清除原始换行 - 用
System.Text.Json序列化后手动包裹:var json = JsonSerializer.Serialize(obj); await writer.WriteAsync($"data: {json} "); await writer.FlushAsync();—— 切记不能用WriteLineAsync直接写 JSON,它自带换行,会破坏消息边界
另外,id: 和 event: 字段非必需,但加了能帮前端做消息去重或类型路由;retry: 要写整数毫秒值(如 retry: 3000),不能带单位也不能是小数。
对接 AI 模型流式 API 时,别直接 CopyToAsync 原始响应体
很多教程教你怎么把 OpenAI 或国内大模型的 text/event-stream 响应原样转发给前端,但实际一上线就出问题:模型返回的 data: 行可能缺空行、带多余注释(: 开头)、甚至混用
。直接透传等于把协议兼容性风险甩给前端。
稳妥做法是「解包再重封」:
- 用
HttpClient请求 AI 接口时,务必加HttpCompletionOption.ResponseHeadersRead,避免整个响应体被缓存 - 逐行读取响应流(
StreamReader.ReadLineAsync()),按冒号切分字段,只保留合法data:内容,丢弃注释行和非法字段 - 对提取出的每段数据做 JSON 安全清洗(比如去掉不可见控制字符、转义换行),再用标准 SSE 格式重写回你自己的
Response.Body - 如果 AI 接口返回的是 chunked JSON(如
{"choices":[{"delta":{"content":"a"}}]}),别自己拼字符串,用JsonDocument.Parse提取delta.content,再序列化成你定义的结构体
本地调试时 EventSource 突然停止接收?先查这三件事
开发阶段最常见的假失败,不是代码错,而是环境干扰:
- IIS Express 默认启用动态内容压缩,会拦截
text/event-stream响应并缓存,关掉:web.config里加<urlcompression dostaticcompression="true" dodynamiccompression="false"></urlcompression> - Chrome 控制台 Network 面板里点开 SSE 请求,看
Response标签页是否持续滚动——如果停在某条消息后不动,大概率是服务端没调FlushAsync或客户端断连后没触发重连 - 前端
EventSource实例没监听error事件,导致连接异常静默失败;加上es.onerror = console.error才能看到真实错误(比如Failed to load resource: net::ERR_INTERNET_DISCONNECTED)
真正难缠的点不在协议本身,而在于 HTTP 中间件链(如认证、CORS、压缩)和反向代理(Nginx、Cloudflare)对长连接的隐式干预。上线前务必在真实网关后跑通端到端流,别只信 localhost。










