应使用 Fiber v3 的 sse.New 中间件而非手动响应,因其自动设置 Content-Type、禁用缓存并实时 flush;c.SendString() 和 c.JSON() 会破坏 SSE 流式语义导致连接关闭。

直接用 sse.New 中间件,别自己拼响应头、手动 flush、写 retry 字段——Fiber v3 的 middleware/sse 已经把传输层细节全封好了,你只需要专注写业务逻辑。
为什么不能直接 return c.SendString() 或 c.JSON()?
SSE 要求响应必须满足三个硬性条件:Content-Type 是 text/event-stream、响应头禁用缓存(Cache-Control: no-cache)、每次写入后必须立即 flush 到客户端。用 c.SendString() 会走默认的 JSON 流程,Content-Type 变成 application/json;c.JSON() 更糟,它还会加 Content-Length 并缓冲整个 body,彻底破坏流式语义。浏览器收到后直接关闭连接,EventSource 状态变成 closed,控制台报错 Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING。
正确做法是让中间件接管整个生命周期:
- 中间件自动设置
Content-Type: text/event-stream和Cache-Control: no-cache - 它把原始
fiber.Ctx包装成*sse.Stream,暴露.Event()、.Comment()、.Retry()等方法 - 每次调用这些方法,底层都触发
http.Flusher.Flush(),确保数据实时抵达浏览器
sse.Config.Handler 里怎么安全写事件?
Handler 函数会在每个新连接建立时被调用一次,传入 stream *sse.Stream。这里不是“发完就结束”,而是要维持连接并持续写入——所以常见错误是写个 for 循环但没加 context 控制,导致 goroutine 泄漏。
务必从 c.Context() 拿 context,并监听取消信号:
Handler: func(c fiber.Ctx, stream *sse.Stream) error {
ctx := c.Context()
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
<pre class="brush:php;toolbar:false;">for {
select {
case <p>}
</p>注意:stream.Event() 返回 error 时,中间件会捕获并终止该连接;不要用 log.Printf 吞掉它,否则你以为还在推,其实早就断了。
如何处理客户端断连而不卡住上游?
Fiber 的 SSE 中间件不依赖心跳包检测断连——它靠 Flush() 的底层 write 错误。当浏览器关闭标签页、网络中断、代理切断空闲连接时,下一次 stream.Event() 或 stream.Comment() 会返回类似 write: broken pipe 或 write: connection reset by peer 的 error。这时候 Handler 函数 return error,中间件自动回收资源,不会残留 goroutine。
但要注意两个坑:
- 别在 Handler 外部起 goroutine 往
stream写,stream不是线程安全的,且生命周期只属于当前 Handler 调用栈 - 如果上游是 LLM 推理服务,别等它吐完所有 token 才往 stream 写——要用
select+ctx.Done()做级联取消,否则用户关页面了,GPU 还在算下一个 token
真正容易被忽略的是 idle timeout:Chrome 对空闲 SSE 连接的强制回收阈值是 47 秒,而 Retry 字段只影响重连间隔,不延长连接存活时间。如果你的业务逻辑偶尔 >45 秒没写数据,必须主动发 stream.Comment("keepalive") 或带空 data 的 event,否则连接静默断开,前端 EventSource 会自动重连,但重连期间的数据就丢了。











