Next.js Rewrite 缓冲导致 SSE 失效:一次 Agent 流式排查记录
在做 Agent 聊天时遇到一个问题:后端 SSE 明明在持续返回,但前端的 Agent 执行过程和回答总是等到最后一次性全部出来,打字机效果完全没有。
最后排查发现,问题出在 Next.js 的 rewrite 代理这一层。
问题
后端日志一直是正常的,每一步 Agent 执行结果都在实时返回,所以一开始主要在排查前端读取和 Python SSE。
后面直接做了一个对比。
直连 FastAPI:
Browser → FastAPI :8080
SSE 正常,前端可以持续收到数据。
经过 Next.js:
Browser → Next.js :3000 → FastAPI :8080
就变成等到最后一次性收到。
所以问题基本可以确定是在 Next.js rewrite 这一层。
当前自建 Next.js 的 rewrites() 链路没有保持 SSE 的增量流式透传。
普通 JSON 请求以前一直没感觉,因为本来就是等完整 Response 回来再处理。
但是 SSE 需要:
FastAPI
↓ chunk 1 → Browser
↓ chunk 2 → Browser
↓ chunk 3 → Browser
↓ chunk 4 → Browser
经过当前 Next.js rewrite 后却变成类似:
FastAPI
↓ chunk 1
↓ chunk 2
↓ chunk 3
↓ chunk 4
Next.js rewrite
↓
缓冲 / 聚合
↓
Browser 一次性收到
这也解释了为什么:
后端日志明明每一步都返回了,前端却还是最后一次性全部显示。
本地怎么处理
本地没有 Caddy,所以对于 SSE 请求直接使用 Python 的真实地址:
Browser :3000
↓
FastAPI :8080
例如:
http://localhost:8080/api/xxx
这样直接绕过 Next.js。
因为 3000 → 8080 跨域,所以 FastAPI 开一下本地 CORS:
http://localhost:3000
最终本地就是:
页面 → Next.js :3000
SSE → FastAPI :8080
生产怎么处理
生产环境本身就有 Caddy。
原来的请求其实走了三层:
Browser
↓
Caddy
↓
Next.js
↓
FastAPI
以前普通 API 这么跑也没问题,所以一直没注意。
现在既然 Next.js 对这些 API 也没有做什么业务,只是单纯 rewrite 转发,直接让 Caddy 分流:
┌── /api/* → FastAPI
Browser → Caddy ─┤
└── /* → Next.js
这样 API:
Browser → Caddy → FastAPI
页面:
Browser → Caddy → Next.js
这里并不是把 Next.js 去掉,而是让 API 请求不再无意义地经过 Next.js。
一开始其实也可以只处理 SSE:
/api/agent/chat → FastAPI
但是想了一下没必要。
如果以后又增加上传、搜索或者其他接口,还要继续改配置。
既然现在 Next.js 对 API 没有额外业务,那直接:
/api/* → FastAPI
/* → Next.js
更简单。
Caddy / Nginx 这里也记一下
现在生产使用的是 Caddy 方便Https。
对于 Content-Type: text/event-stream 的 SSE 响应,Caddy 可以及时刷新流数据,所以目前这条链路:
Browser → Caddy → FastAPI
不需要再额外关闭 buffering。
如果以后换成 Nginx,要记得注意代理缓冲,例如:
location /api/ {
proxy_pass http://server:8080;
proxy_buffering off;
}
否则可能刚绕过 Next.js,又被 Nginx 缓冲了:
FastAPI 在流
↓
Nginx 缓冲
↓
Browser 一次性收到
顺便搞明白了 Next.js 这一层到底要不要
之前会觉得 Next.js 是全栈框架,API 是不是都应该先经过 Next.js。
这次之后感觉不能这么理解。
如果 Next.js 真正在做:
- BFF 数据聚合
- Cookie / Session 处理
- 鉴权
- API Key 隐藏
- 多个后端接口聚合
那:
Browser → Next.js → FastAPI
这一层当然有意义。
现在 Next.js 对 API 什么都没干,只是转发一下,没必要为了“统一”强行多绕一层。
目前结构变成:
┌── Next.js
│ 页面 / SSR
Browser → Caddy ─┤
│
└── FastAPI
API / Agent / SSE
职责反而更清楚了。