Next.js 代理陷阱

SKILL开发
开发 未标注权限 许可证不明确
安装这个技能

修好并写对 Next.js 的 API 代理(next.config 的 rewrites 与 route handler 转发到内部 FastAPI 类后端),避开一串经典陷阱:只在代理侧出现的 401 与 504、经代理才失败的大文件上传、SSE 一次性到达、换后端源站必须重新构建、CORS 泄漏内部主机名、代理 JSON 乱码、fetch failed 与 UND_ERR_NOT_SUPPORTED。覆盖构建期环境固化、约 30 秒的 rewrite 上限、Expect 100-continue、二次解压、尾斜杠重定向泄漏内网、catch-all 路由的路径穿越、客户端中断与 AbortSignal 超时。

一句话选型结论

按资料字段整理

Next.js 代理陷阱是开发类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类开发 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_617dd917/nextjs-proxy-traps-moran
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

开发
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_617dd917仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验2026-11-06上游最后核验时间 · 数据更新 2026-11-06
正文体量21.5 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
写代码与改 bug

按需求补代码、定位并修掉报错。

读仓库与生成改动

让模型读懂项目结构,再给出可落地的改动。

跑测试与排查故障

按现象排查问题,补测试用例并复跑。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在开发场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
Next.js 代理陷阱当前 未标注权限 未标注 未标注 未标注 有
技能审查员 浏览器控制 ClawHub 市场 877.9 万 OpenClaw 有
Vercel React 最佳实践 浏览器控制 GitHub 仓库 849.1 万 Codex、Claude、Cursor 有
GitHub 代码协作 联网访问 ClawHub 市场 763.3 万 OpenClaw 有
网页设计指南 联网访问 GitHub 仓库 668.6 万 Codex、Claude、Cursor 有
本体论 只读 ClawHub 市场 663.7 万 OpenClaw 有

SKILL.md 全文

21.5 KB
模型实际读到的说明书原文,来自技能仓库

Next.js 代理陷阱 — BFF 代理到内部后端

你正在构建或排查一个充当 API 网关 / BFF 的 Next.js 应用:浏览器只和 Next.js 说话,Next.js 再通过 next.config.ts 的 rewrites 或 catch-all route handler 把请求转发给内部后端服务(FastAPI、Express、内部微服务)。这个角色自带一族特定陷阱,其症状几乎总把矛头指向代理之外的地方:后端"超时"、上传"随机失败"、CORS"坏了",或者内部主机名"漏"进了浏览器。先在下文找到匹配的陷阱,再去怪后端。

目录

  • 何时使用
  • 参数与占位符
  • 症状索引
  • 心智模型:两种代理机制
  • 错误处理约定
  • 失败模式与恢复
  • 不适用场景
  • 陷阱 1 — rewrite 目标地址在构建期就被固化
  • 陷阱 2 — 内置 rewrite 代理在约 30 秒处截断
  • 陷阱 3 — Expect: 100-continue 打崩大文件上传
  • 陷阱 4 — 二次解压把代理回来的响应弄坏
  • 陷阱 5 — 尾斜杠重定向泄漏内部后端主机名
  • 实践 A — 在 catch-all 代理里过滤路径穿越

何时使用

自动触发:用户描述与下表场景相关时直接启用,不需要点名本技能。

用户会这么说 实际场景
「后端地址改了,重启了还是打旧 host」 构建期固化 → 陷阱 1
「超过 30 秒的请求只有走代理才超时」 rewrite 内置上限 → 陷阱 2
「大文件上传报 fetch failed,小文件没事」 Expect: 100-continue → 陷阱 3
「代理回来的 JSON 乱码了」 二次解压 → 陷阱 4
「控制台报 CORS,里面还有个内网域名」 尾斜杠重定向泄漏 → 陷阱 5
「SSE 一次性全出来,停止按钮没用」 流式被缓冲 → 实践 B

手动调用:对话里说「用 Next.js 代理陷阱处理」,或直接用 @ 引用本技能(平台的完整 slug 可能带作者后缀,不必写全名)。

不适合:纯客户端 fetch 问题、Next.js 之外的 nginx 等反代、只返静态数据的 route——见「不适用场景」。

参数与占位符

动手前先替换下表占位符;未替换就跑等于用错环境。

占位符 含义 从哪里取
<proxy-host> 代理对外域名 部署清单里 web 入口的域名
<backend-host> 后端内部主机名 容器内 getent hosts backend
<path> 被代理的路由段 app/api/[...path]/route.ts 的挂载前缀
<proxy-container> 代理容器名 docker compose ps --format '{{.Name}}'

症状索引

症状 看哪节
在 .env 里换了后端源站、重启了,请求还是打到旧主机 陷阱 1
只有经代理时,超过约 30 秒的请求才超时而死 陷阱 2
超过约 1MB 的上传失败,报 "fetch failed" / UND_ERR_NOT_SUPPORTED 陷阱 3
代理回来的 JSON 乱码,或报 content-encoding / 长度不匹配 陷阱 4
浏览器 CORS 报错里出现内部后端主机名 陷阱 5
构造的 /proxy/../../admin 路径打到了不该到的后端路由 实践 A
SSE 一次性全部到达;"停止"按钮停不下后端 实践 B
需要一个同时尊重客户端断开的超时 实践 C

心智模型:两种代理机制

  • rewrites(在 next.config.ts 里):零代码,每条路由一行。代价是:destination 的 env 值在构建期求值(陷阱 1)、固定的约 30 秒内部超时(陷阱 2)、默认的请求体大小上限,以及对头部的零控制权——这让陷阱 3–5 在 rewrite 里没法修。
  • route handler catch-all(app/**/[...path]/route.ts):真正的代码。在服务端运行时读 process.env,自己设超时,双向控制每一个头部。

决策规则:短、小、发完就不管的内部端点可以走 rewrite。任何长时间运行的(LLM 调用、文档转换、报表)、流式的(SSE)、或重上传的,都走 route handler。真实事故大多以"把这条路由挪到 route handler"收场。

错误处理约定

本技能把出错分成四类,处置与重试策略不同,不要混着来:

类别 典型情况 处置 可重试
输入错误 参数缺失、路径或格式不对 直接拒绝,指出缺哪个字段 否
环境错误 权限、网络、依赖服务不可用 给检查命令,停下等修复 修复后
证据不足 日志、指标、状态缺失 先补证据,不做判断 补证据后
结论冲突 多个判据指向不同根因 列冲突点,先安全后性能 否

统一输出格式(每次给结论都按这四段写,便于他人复核):

判据:<看到的现象与具体取值>
结论:<属于哪一类 / 最可能的根因>
动作:<最小修改,或需要补的证据>
验证:<怎么确认修好了>

本技能常见错误码:

代码 现象 处置
E-ENV-01 401 / 504 只在代理侧出现 长请求迁到 route handler 并自管超时(陷阱 2)
E-ENV-02 大文件上传 UND_ERR_NOT_SUPPORTED 转发前删除 expect 头后重建(陷阱 3)
E-ENV-03 代理回来的 JSON 乱码 删 content-encoding 与 content-length(陷阱 4)
E-ENV-04 浏览器里出现内网主机名 保留尾斜杠,或设 skipTrailingSlashRedirect(陷阱 5)
E-ENV-05 认证失败(401) 修头透传;密钥别放前端

输入校验清单、重试与退出语义见 references/error-handling.md。

失败模式与恢复

失败现象 常见原因 检查方法 恢复动作 可否重试
401 / 504 只在代理侧出现 头未透传,或触到 rewrite 30 秒上限 curl -v 直连后端与经代理各打一次,比状态码与耗时 长请求迁到 route handler 并自管超时(陷阱 2) 可重试
大文件上传 UND_ERR_NOT_SUPPORTED Expect 头透传给了 undici curl -v -H "Expect: 100-continue" -F file=@big.bin https://<proxy>/upload 转发前删除 expect 头后重建(陷阱 3) 可重试
代理回来的 JSON 乱码 复制了上游 content-encoding curl -sI https://<proxy>/<path> 看响应头 删 content-encoding 与 content-length(陷阱 4) 可重试
浏览器里出现内网主机名 尾斜杠 307 带出 Location curl -sI https://<proxy>/<path>/ 看 Location 保留尾斜杠,或设 skipTrailingSlashRedirect(陷阱 5) 可重试
认证失败(401) token 未透传或已过期 curl -H "Authorization: Bearer <t>" https://<proxy>/<path> 修头透传;密钥别放前端 可重试

失败时优先中止:改代理代码前先用 curl 复现并留存证据;一旦发现请求打到了错误的后端实例,先停手确认目标,不要继续改配置。

不适用场景

  • 纯粹的客户端 fetch 问题,压根不碰 Next.js 服务端代码(浏览器侧 CORS 处理、直连后端调用)。
  • Next.js 之外的反向代理——nginx、Traefik、Caddy 有自己的旋钮和故障模式。
  • 只返回静态数据、从不转发上游的 API route。

陷阱 1 — rewrite 目标地址在构建期就被固化

症状。 你在 .env 里改了后端源站,重启服务或容器,请求还是打到旧主机。与此同时,读同一个变量的 route handler 却拿到了新值,这让问题更迷惑。客户端 bundle 里的 NEXT_PUBLIC_* 值同样如此。

原因。 next.config.ts 在 next build 运行时求值。把 process.env 插值进 rewrite destination 的模板字符串在那一刻就被解析,并写进构建产物(routes manifest)。生产容器镜像只构建一次;重启容器只是重放已固化的 manifest。NEXT_PUBLIC_* 在同一次构建里被内联进客户端 JS。route handler 之所以有效,是因为它模块级的 process.env 读取发生在服务端进程启动时,而不是镜像构建时。

修复。
- 把任何与 rewrite 相关的 env 变更都当成需要 rebuild,而不是 restart。
- 配置缺失就显式失败:加载配置时,收集每一个回落到默认值的 origin,打一条聚合警告。静默的 ?? "http://127.0.0.1" 兜底,正是一个过期 origin 在生产里长期不被发现的方式。
- 如果某个 origin 必须在无法 rebuild 的情况下变更,把该路由挪到 route handler,在模块加载时读 process.env。

陷阱 2 — 内置 rewrite 代理在约 30 秒处截断

症状。 合法运行超过半分钟的端点——LLM 补全、文档转换、批处理任务——只有经 rewrite 调用时才超时。同一个请求用 curl 直连后端能跑完。

原因。 rewrite 代理有一个内部超时,实测约 30 秒(受 Next.js 版本与运行时影响,不同版本阈值可能不同,以本地复现为准)。它不可配置,所以任何可能超过它的端点都需要代码级代理。

修复。 把长路由挪到 catch-all route handler,并把超时攥在自己手里:

export const maxDuration = 600;           // platform hard ceiling (e.g. serverless)
const TIMEOUT_MS = 9 * 60 * 1000;         // keep strictly under the ceiling
const upstream = await fetch(target, { signal: AbortSignal.timeout(TIMEOUT_MS) });

把 maxDuration 设成平台的硬上限,把 fetch 超时设在它之下。真正触发的是 fetch 的信号;maxDuration 只是防止平台先一步把函数杀掉。

陷阱 3 — Expect: 100-continue 打崩大文件上传

症状。 经代理、超过约 1MB 的上传失败并报 fetch failed;服务端日志显示 cause 为 UND_ERR_NOT_SUPPORTED。经代理的小文件正常,同一个大文件直传后端也正常。

原因。 curl(以及一些 HTTP 客户端)会给超过约 1MB 的表单上传加上 Expect: 100-continue,意思是"服务端同意之前先别发请求体"。代理把这个头原样转发给自己发出的 fetch,而 Node 的 fetch(undici)没有实现 100-continue 握手——它直接抛错而不是等待。证据之所以让人抓狂是必然的:curl 直连能成,是因为 curl 自己会说这个握手;经代理时,这个头落到 undici 手里就死了。

修复。 转发前把这个头删掉——内部代理从握手里得不到任何好处:

const h = new Headers(request.headers);
h.delete("expect");   // undici has no 100-continue support; forwarding it throws

相关的旋钮:经内置 rewrite 代理的上传还受默认请求体大小上限约束(experimental.proxyClientMaxBodySize,例如 "64mb"——已在 Next.js 16.1.6 上验证;它是 experimental 选项,依赖它之前先确认你的 Next 版本里有)。这也是重上传该放进 route handler 的又一个理由:那里你完全掌控 body。

陷阱 4 — 二次解压把代理回来的响应弄坏

症状。 代理回来的 JSON 乱码,或浏览器抛出内容解码错误,有时是长度不匹配错误。直接检查后端响应却是健康的。

原因。 Node 的 fetch 会透明地解压 gzip/br/deflate 的响应体——但如果你把上游的响应头复制到自己的 Response 上,你就把原始的 content-encoding: gzip(以及已经过期的 content-length)转发出去,它们描述的是已不存在的字节。浏览器照着头来,试图对已经是明文的字节再做一次 gunzip。

修复。 fetch 之后,从响应头里删掉这两个头,让运行时重新计算。顺手把那些描述错误连接的 hop-by-hop 头也去掉:

const out = new Headers(upstream.headers);
out.delete("content-encoding");   // body is already plain; the header triggers a second decode
out.delete("content-length");     // describes the compressed size, not the body you return
out.delete("connection");
out.delete("keep-alive");
out.delete("transfer-encoding");

陷阱 5 — 尾斜杠重定向泄漏内部后端主机名

症状。 浏览器控制台出现针对后端内部主机名或 docker 网络名的 URL 的 CORS 失败——那个地址浏览器本不该知道。只在某些端点上发生。

原因。 重建上游路径的代理容易把尾斜杠丢掉。FastAPI 对斜杠不匹配的路径回 307 redirect_slashes,并用自身 origin——内部主机名——构造 Location 头。代理用 redirect: "manual" 取上游(这是正确设置),于是那个 307 连同 Location 直接透传给浏览器;浏览器随后从用户机器上跟随 Location,直连内部地址。请求失败(外部不可达、也没有 CORS 头),而控制台把你的内部拓扑广播了出去。失败的端点恰好就是后端路由登记了尾斜杠的那些。

修复。 构造目标 URL 时保留原请求的尾斜杠,让重定向根本不触发:

if (request.nextUrl.pathname.endsWith("/") && !target.pathname.endsWith("/")) {
  target.pathname += "/";
}

让代理的 fetch 保持 redirect: "manual",这样上游重定向仍然是显式、可调试的,而不是服务端悄悄重发请求;同时考虑在 next.config 里设 skipTrailingSlashRedirect: true,免得 Next.js 自己抢在你前面注入它自己的斜杠重定向。


实践 A — 在 catch-all 代理里过滤路径穿越

[...path] 路由会转发 URL 里的一切。像 /proxy/backend/../../admin 这样的请求——或它的编码形式 %2e%2e——会在构造目标 URL 时被 new URL(...) 规范化,从而逃出预期前缀,代理到任意后端路径(管理面板、内部调试路由)。构造 URL 之前先拦截:只要出现等于 .. / . 的路径段、或大小写不敏感地含 %2e 的段,直接返回 4xx 并终止(不要静默过滤后继续转发——那会让攻击者用编码形式绕过检查)。五行代码;没有它们,这个代理就是通往后端背后一切的大门。

实践 B — 透传 SSE,并把客户端中断记为 499

缓冲会杀死流式:SSE 响应会在结尾一次性冲出来(或者永远不出来),"停止生成"按钮也不可能生效。识别流式并原样透传响应体:

const isSSE = (upstream.headers.get("content-type") ?? "").includes("text/event-stream");
const body = isSSE ? upstream.body : Buffer.from(await upstream.arrayBuffer());
return new Response(body, { status: upstream.status, headers });

非 SSE 响应则刻意缓冲:把分块的普通响应体透传出去会与自动解压相互干扰,还可能把它截断。要让"停止"真正生效,在客户端断开时中止上游 fetch,并返回 499(nginx 对"客户端关闭请求"的约定),这样中断不会以 502 的形式污染错误日志:

if (request.signal.aborted) return new Response(null, { status: 499 });

实践 C — 用 AbortSignal.any 组合中断信号

一个被代理的请求有两个彼此独立的中断理由:你自己的超时,以及客户端离开。两者都应该中止上游 fetch——否则一个已断开的客户端会让后端 LLM 继续生成好几分钟。把两者合成一个信号:

const signal = AbortSignal.any([request.signal, AbortSignal.timeout(TIMEOUT_MS)]);

对完全缓冲的路由,客户端断开信号很少在传输中途触发,但把它带上无害,而且能让所有情况走同一条代码路径。

现成模板

references/proxy-handler-template.ts 是一个自包含、带注释的 catch-all route handler,把上面所有内容合在一起:env 驱动的 origin、Expect 去头、请求/响应头卫生、尾斜杠保留、穿越过滤、SSE 透传、499 中断、组合超时。实现新的代理路由时读它,或当线上事故命中任一陷阱时读它——每个代码块都带一行 why 注释,方便你只摘取需要的那几段。

模板的三条使用注意

  1. 方法覆盖:Next.js 只认你显式导出的同名函数(export const GET/POST/PUT/PATCH/DELETE)——没有「一个 methods 数组」这回事。必须再导出 OPTIONS(漏了跨域预检直接 405)与 HEAD(漏了健康检查类探针会失败);模板已包含这两个导出。
  2. 大请求体不要整体进内存:模板对 content-length > 1MB 的请求改用 request.body 流式透传,并带 duplex: 'half'(Node 18+ 的 fetch 要求);小请求仍用 arrayBuffer(),因为流式时无法预知长度、会退化成 chunked。
  3. 默认源站会掩盖配置缺失:模板带了兜底源站,未配置目标时请求会「看起来正常」地打到默认地址。上线前请删掉兜底值或改成显式报错,避免静默走错后端。

完整走查(端到端样例)

走查一:大文件经代理上传报 fetch failed(陷阱 3)

场景:大文件经 Next.js 代理上传报 fetch failed,直连后端却成功(陷阱 3)。

输入:用户原话「上传超过 1MB 就失败,报 fetch failed,但直接打后端接口是好的」。

curl -v --max-time 60 -H "Expect: 100-continue" -F "file=@big.bin" https://<proxy-host>/upload
curl -s -o /dev/null -w '%{http_code}\n' -F "file=@big.bin" http://<backend-host>:8000/upload

中间输出(示例):

> POST /upload HTTP/1.1
> Expect: 100-continue
> Content-Length: 12582912
< HTTP/1.1 500 Internal Server Error
< content-type: application/json
{"error":"fetch failed","cause":{"code":"UND_ERR_NOT_SUPPORTED"}}

200

定位与修复:直连 200、经代理 500,且 cause.code 是 UND_ERR_NOT_SUPPORTED → 代理把 Expect 头透传给了 undici →

  1. 转发前 headers.delete("expect")(陷阱 3);
  2. 把 err.cause 打进日志,下次一眼定位;
  3. 重建或重启代理层。

修复后复测:

$ curl -s -o /dev/null -w '%{http_code}\n' -F "file=@big.bin" https://<proxy-host>/upload
200
$ docker logs <proxy-container> --tail 2
POST /upload 200 3.2s 12.4MB

结论:验证陷阱 3——「直连成功 ≠ 经代理成功」时先怀疑头,再怀疑后端。

走查二:尾斜杠重定向把内部主机名泄漏给公网(陷阱 5)

输入:用户原话「浏览器控制台报 CORS,报错里那串地址是我们内网的 backend.internal」。

curl -s -o /dev/null -D - https://app.example.com/api/proxy/users/
curl -s -o /dev/null -D - https://app.example.com/api/proxy/users

中间输出(示例):

HTTP/1.1 307 Temporary Redirect
location: http://backend.internal:8000/users/
content-length: 0

HTTP/1.1 200 OK
content-type: application/json

定位:不带尾斜杠的请求 200,带尾斜杠的被后端 307 到内部主机名——代理重建 target 时把尾斜杠丢了,FastAPI 的 redirect_slashes 用自身 origin 构造 Location,代理又用 redirect: "manual" 把它透传给浏览器(陷阱 5)。先排除 CORS 配置:响应里根本没有 CORS 头,问题在重定向而不是跨域设置。

修复:构造 target 时保留原请求的尾斜杠(target.pathname += "/"),或在 next.config 里设 skipTrailingSlashRedirect: true。

复测:

curl -s -o /dev/null -D - https://app.example.com/api/proxy/users/
HTTP/1.1 200 OK
content-type: application/json

结论:验证陷阱 5 的判据顺序——先看 Location 里有没有内部主机名,再怀疑 CORS 配置。

分工:走查一覆盖陷阱 3(请求头方向),走查二覆盖陷阱 5(响应头方向),一个是「上游收不到」,一个是「内部地址漏出去」。

事故检查清单

当"代理坏了"时,按这个顺序查:

  1. env 变更真的进到构建里了吗?(陷阱 1)
  2. 请求跑了多久才死——大约 30 秒?(陷阱 2)
  3. 失败的请求带 Expect 头吗?请求体超过约 1MB 了吗?(陷阱 3)
  4. 响应里还声称 content-encoding: gzip 吗?(陷阱 4)
  5. 浏览器里失败的 URL 含内部主机名吗?(陷阱 5)
  6. 改代理代码之前,先用 curl 直接打后端复现——它能隔离出是哪一侧在骗你。

延伸阅读

references/error-handling.md

常见问题

5 问
答案由本站依据条目字段整理
Next.js 代理陷阱怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_617dd917/nextjs-proxy-traps-moran。安装方式为未标注。装完刷新技能列表或重开客户端。

Next.js 代理陷阱支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

Next.js 代理陷阱权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

Next.js 代理陷阱收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

Next.js 代理陷阱用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 2026-11-06。