DaemonDaemonAI LAB
DAEMON / ARTICLE

Next.js Rewrite 缓冲导致 SSE 失效:一次 Agent 流式排查记录

2026.09.16 · 7 MIN READ

在做 Agent 聊天时遇到一个问题:后端 SSE 明明在持续返回,但前端的 Agent 执行过程和回答总是等到最后一次性全部出来,打字机效果完全没有。

最后排查发现,问题出在 Next.js 的 rewrite 代理这一层

问题

后端日志一直是正常的,每一步 Agent 执行结果都在实时返回,所以一开始主要在排查前端读取和 Python SSE。

后面直接做了一个对比。

直连 FastAPI:

text
Browser → FastAPI :8080

SSE 正常,前端可以持续收到数据。

经过 Next.js:

text
Browser → Next.js :3000 → FastAPI :8080

就变成等到最后一次性收到。

所以问题基本可以确定是在 Next.js rewrite 这一层。

当前自建 Next.js 的 rewrites() 链路没有保持 SSE 的增量流式透传。

普通 JSON 请求以前一直没感觉,因为本来就是等完整 Response 回来再处理。

但是 SSE 需要:

text
FastAPI
 ↓ chunk 1 → Browser
 ↓ chunk 2 → Browser
 ↓ chunk 3 → Browser
 ↓ chunk 4 → Browser

经过当前 Next.js rewrite 后却变成类似:

text
FastAPI
 ↓ chunk 1
 ↓ chunk 2
 ↓ chunk 3
 ↓ chunk 4

Next.js rewrite
 ↓
缓冲 / 聚合
 ↓
Browser 一次性收到

这也解释了为什么:

后端日志明明每一步都返回了,前端却还是最后一次性全部显示。


本地怎么处理

本地没有 Caddy,所以对于 SSE 请求直接使用 Python 的真实地址:

text
Browser :3000
     ↓
FastAPI :8080

例如:

text
http://localhost:8080/api/xxx

这样直接绕过 Next.js。

因为 3000 → 8080 跨域,所以 FastAPI 开一下本地 CORS:

text
http://localhost:3000

最终本地就是:

text
页面 → Next.js :3000

SSE  → FastAPI :8080

生产怎么处理

生产环境本身就有 Caddy。

原来的请求其实走了三层:

text
Browser
   ↓
Caddy
   ↓
Next.js
   ↓
FastAPI

以前普通 API 这么跑也没问题,所以一直没注意。

现在既然 Next.js 对这些 API 也没有做什么业务,只是单纯 rewrite 转发,直接让 Caddy 分流:

text
                 ┌── /api/* → FastAPI
Browser → Caddy ─┤
                 └── /*     → Next.js

这样 API:

text
Browser → Caddy → FastAPI

页面:

text
Browser → Caddy → Next.js

这里并不是把 Next.js 去掉,而是让 API 请求不再无意义地经过 Next.js

一开始其实也可以只处理 SSE:

text
/api/agent/chat → FastAPI

但是想了一下没必要。

如果以后又增加上传、搜索或者其他接口,还要继续改配置。

既然现在 Next.js 对 API 没有额外业务,那直接:

text
/api/* → FastAPI
/*     → Next.js

更简单。


Caddy / Nginx 这里也记一下

现在生产使用的是 Caddy 方便Https。

对于 Content-Type: text/event-stream 的 SSE 响应,Caddy 可以及时刷新流数据,所以目前这条链路:

text
Browser → Caddy → FastAPI

不需要再额外关闭 buffering。

如果以后换成 Nginx,要记得注意代理缓冲,例如:

nginx
location /api/ {
    proxy_pass http://server:8080;
    proxy_buffering off;
}

否则可能刚绕过 Next.js,又被 Nginx 缓冲了:

text
FastAPI 在流
     ↓
Nginx 缓冲
     ↓
Browser 一次性收到

顺便搞明白了 Next.js 这一层到底要不要

之前会觉得 Next.js 是全栈框架,API 是不是都应该先经过 Next.js。

这次之后感觉不能这么理解。

如果 Next.js 真正在做:

那:

text
Browser → Next.js → FastAPI

这一层当然有意义。

现在 Next.js 对 API 什么都没干,只是转发一下,没必要为了“统一”强行多绕一层。

目前结构变成:

text
                 ┌── Next.js
                 │   页面 / SSR
Browser → Caddy ─┤
                 │
                 └── FastAPI
                     API / Agent / SSE

职责反而更清楚了。