在 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)

前端浏览器在接收到响应时陷入混乱:

  1. 浏览器读取到 Content-Encoding: gzip,误以为收到的明文是压缩包,尝试进行 gzip 解压,结果解压失败并抛出解码错误。
  2. 即使部分客户端绕过了解码校验,读取到 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 代理转发时,需要牢记几个关键结论:

  1. 服务端 fetch 默认会尝试协商并自动解压返回的内容。
  2. 第三方 API 响应的头部信息不能盲目全量透传给前端浏览器。
  3. 转发响应时必须剥离底层的编码与长度声明,将传输控制权交还给当前的渲染服务器。