在 Next.js 服务端环境开发接口代理或者转发路由,直接透传第三方 API 响应是,前端收到 ERR_CONTENT_DECODING_FAILED 错误、返回数据变成乱码,或者浏览器因为长度不符而提前中断连接。
代理转发中的异常表现
在一个标准的 Next.js Route Handler 中,为了将外部接口的数据无缝转发给前端,最初的代码通常编写为直接读取并返回响应体:
// app/api/proxy/route.ts
export async function GET() {
const targetUrl = 'https://third-party-api.com/data';
const response = await fetch(targetUrl);
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
}
这段逻辑在本地测试时偶尔正常,但在高并发或生产环境中频繁触发错误:
- 浏览器报错:
net::ERR_CONTENT_DECODING_FAILED或协议解析错误。 - 数据截断:响应体只包含一部分 JSON 数据,后续内容丢失。
- 排查比对:通过命令行工具直接请求第三方接口时,返回数据完好无损,证明问题出在 Next.js 的转发逻辑中。
底层机制剖析与透明解压原理
问题的根源在于 Next.js 服务端 fetch 的底层行为与 HTTP 标准规范的碰撞。
1. 默认的压缩请求头
在现代 Node.js 和 Next.js 运行时中,内置的 fetch 基于标准的网络模块实现。当服务端发起请求时,底层客户端会自动添加如下头部:
Accept-Encoding: gzip, deflate, br
此设计用于在服务器之间传输时减少带宽消耗。
2. 透明解压的执行过程
第三方服务器接收到带有 Accept-Encoding: gzip 的请求后,会将响应内容压缩,并在响应头中返回:
Content-Encoding: gzip
Content-Length: 1024
此处的 Content-Length 代表压缩后的字节大小(例如 1KB)。
然而,Next.js 服务端的 fetch 内部集成了透明解压(Transparent Decompression)机制。当开发者通过 response.body 获取数据时,底层已经自动完成了 gzip 解压,暴露给代码的是解压后的完整明文数据(例如 5KB)。
响应头与实体内容的错位冲突
当代码将原封不动带有旧响应头的 response.headers 返回给前端时,灾难发生了。
- 实际输出的 Body:5KB 的解压后明文。
- 透传过去的 Headers:
Content-Encoding: gzip(声明内容是压缩包)Content-Length: 1024(声明长度是 1KB)
前端浏览器在接收到响应时陷入混乱:
- 浏览器读取到
Content-Encoding: gzip,误以为收到的明文是压缩包,尝试进行 gzip 解压,结果解压失败并抛出解码错误。 - 即使部分客户端绕过了解码校验,读取到
Content-Length: 1024后也会强制切断连接,导致 5KB 的明文数据被严重截断。
在标准的 Web fetch 规范中,并没有提供直接关闭透明解压的开关,开发者无法直接获取未解压的原始字节流。因此,顺应解压行为并重构响应头成为了唯一的正解。
规范化处理与标准解决方案
最优雅的处理方式是在转发前剔除与传输编码相关的旧头部,交由托管环境重新协商传输格式。
代码实现方案
在构造返回给浏览器的 Response 对象时,手动删除失效的头部信息:
// app/api/proxy/route.ts
export async function GET() {
const targetUrl = 'https://third-party-api.com/data';
const response = await fetch(targetUrl);
const newHeaders = new Headers(response.headers);
// 清除失效的传输编码与长度声明
newHeaders.delete('content-encoding');
newHeaders.delete('content-length');
newHeaders.delete('transfer-encoding');
// 保留业务类型与缓存控制头部
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
方案执行优势
- 格式识别准确:保留了原有的
Content-Type(如application/json),确保前端能够正确解析数据类型。 - 启用分块传输:移除了
Content-Length之后,Node.js 运行时会自动采用标准的分块传输(Transfer-Encoding: chunked),彻底根除长度不一致引起的截断问题。 - 链路压缩优化:明文流交回给 Next.js 托管服务器后,生产环境的边缘网络或反向代理(如 Vercel、Nginx)会根据浏览器的支持情况,自动重新应用 Brotli 或 Gzip 压缩,兼顾了传输性能与数据安全。
总结
在 Next.js 服务端处理 API 代理转发时,需要牢记几个关键结论:
- 服务端
fetch默认会尝试协商并自动解压返回的内容。 - 第三方 API 响应的头部信息不能盲目全量透传给前端浏览器。
- 转发响应时必须剥离底层的编码与长度声明,将传输控制权交还给当前的渲染服务器。