Next.js RSC _rsc参数丢失的那些坑以及解决方案

最近在用 Next.js 的 RSC(React Server Component)时遇到了一个比较隐晦的问题,记录一下踩坑过程和应对方案。

如果你在项目里既使用了 CDN(并且缓存了 html),又使用了middleware的重定向(middleware 会处理 rsc 请求),又用了 RSC 特性(开启了 prefetch 等情况),如果你发现部分页面 html 变成了一大串“乱码”(其实是 rsc 请求的返回结果),可以仔细看看。

RSC 请求是怎么工作的?

Next.js 在请求 RSC 和 HTML 页面时,路径和方法其实是一样的,唯一的区别在于它会带上一个 _rsc 的参数和一些特定 header。

比如你访问页面 /about

  • HTML 请求
    GET /about
  • RSC 请求
    GET /about?_rsc=<随机字符串> 以及特定的 header

这里的 _rsc 参数是 Next.js 内部用来标识“这是一次 RSC 请求”,header 里也有类似 Next-Router-State-Tree, rsc 等用于数据请求和分割的内容。

Next.js 为什么要“吞掉”这些参数?

你以为既然客户端发出了带 _rsc 参数的请求,那么在 middleware 下我们理应可以拿到。但实际情况是——拿不到

原因就在于:
Next.js 觉得这些属于框架内部的数据传递(不想暴露给开发者),所以默认会把这些 RSC 相关的参数和 header 给“吞掉”,即在 middleware 里拿不到。(测试发现当前版本,即使是正常的 html 请求,这些固定的参数也都会被抹去的,所以不要和官方内部使用的名字撞车)

你可能会发现无论用 request.nextUrl.searchParams.get('_rsc') 还是直接获取 header,都看不到 RSC 的直接痕迹。

为什么会出问题?

在大多数普通场景下,这其实无所谓。但是,有三个条件叠加之后问题就来了:

  1. 前面有 CDN,并且 CDN 的缓存 key 用 URL+参数 规则
  2. CDN 规则里会缓存了部分页面的 HTML(如静态页面)
  3. 在 middleware 里做这些页面的重定向(比如国际化/页面迁移等等情况)

此时,RSC 的请求本身是 /about?_rsc=xxxx,但我们在 middleware 做重定向或者做其它处理时,参数被吃掉,最后变成了 /about

这样,CDN 就完全分不清楚,RSC 请求和正常 HTML 请求是一样的!
比如:

  • 客户端请求: /about?_rsc=1&a=1&b=1
  • 重定向,经过 middleware 被 Next.js 干掉后,变成 Location /about?a=1&b=1
  • 缺少了_rsc 参数,CDN 如果只根据 url 和 search param 缓存的话就混淆了

这时你就会发现:刷新页面偶尔还能刷出一堆 RSC 的结果,页面展现一团乱麻

在 Middleware 里还能区分吗?

那我还能不能从 Middleware 里判断这个请求到底是 RSC 还是 HTML?

结论是——很难稳定区分

  • search params 没了(被吞了)
  • headers 里 RSC 的标记也没了(被吞了)
  • 只剩下 accept header:
    • HTML 请求一般是在accept头中包含text/html
    • RSC 请求则是 accept: */*

看似可用,实际上这个特征又太草率,万一其它请求也发 */* 呢?总觉得不靠谱。

解决方案——利用 Nginx 兜底

目前我的解决办法就是前置 Nginx。在 Nginx 里为 _rsc 这个参数专门映射一个自定义 header,然后转发给 Next.js。

Nginx 配置示例:

1
2
3
4
# http块内 和server块同级
map $arg__rsc $rsc_header {
default $arg__rsc;
}

location 块中

1
2
3
4
5
6
7
location / {
proxy_set_header 这里是自定义header名 $rsc_header;

proxy_pass http://127.0.0.1:3000;

...
}

然后在 nextjs 中给他放回去: (我这里的重定向是用的 next-intl 的 middleware)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const nextIntlMiddleware = createMiddleware(routing);

export default async function middleware(request: NextRequest) {
const url = new URL(request.url);
// run intl
const response = nextIntlMiddleware(request);

const location = response.headers.get("Location");
if (location) {
const redirectUrl = new URL(location || "", url.origin);

const rscValue = request.headers.get("这里是自定义header名");
console.log(
"middleware get rsc value",
"location",
location,
"rscValue",
rscValue
);

if (rscValue) {
// 给他塞回去
redirectUrl.searchParams.set("_rsc", rscValue);
response.headers.set("Location", redirectUrl.toString());
}
}

return response;
}

这样,在后端中间件里就能通过自定义请求头判断请求来源,不会再意外丢失参数。

总结

Next.js 的 RSC 请求,本质上和 HTML 请求是同一路径,只是带了内部参数用于区分。CDN 缓存规则如果不区分 _rsc,就会出现缓存混乱,这对生产系统来说是很大的隐患。

最佳实践建议:

  • 对静态页面要么彻底关闭 CDN 缓存,要么配置 CDN 支持根据 header 区分请求(需要 CDN 支持)
  • 或者在 Nginx 层把 RSC 参数投递到自定义 header,后续逻辑用 header 判断

这也说明了 Next.js 的黑盒问题,很多细节都需要自己去处理,在vercel部署没问题的,自己自托管可能就会出问题。还是比较蛋疼的。

参考链接

Rsc header and query params always null on middleware #65787