一次 Postman 报错 Unexpected End of File 的排查复盘

最近遇到一个挺绕的接口问题:Postman 请求状态码明明是 200,但就是没有响应数据,同时还会报一个看起来很奇怪的错误:

1
Error: unexpected end of file

一开始看到这个报错,很容易怀疑是服务端没启动、返回体格式不对,甚至是接口代码本身有异常。但这次真正的问题,不在业务逻辑,而是在“测试环境请求链路 + 压缩编码 + 代理转发”这几个环节叠在了一起。

这篇文章就把这次排查过程完整梳理一下,重点讲清楚这几个问题:

  1. 为什么 curl 能正常返回,而 Postman 不行
  2. 为什么有些接口正常,只有部分接口出问题
  3. 为什么最终定位到的是 content-encoding: br
  4. 测试环境里的反向代理到底起了什么影响
  5. 最后是怎么处理掉的

1. 现象是什么:状态码 200,但是没有响应数据

最开始碰到的问题很怪:

  • Postman 请求接口返回 200
  • 但是响应体没有正常展示出来
  • 同时报错:Error: unexpected end of file

这类问题最麻烦的地方在于,它不是典型的 4xx / 5xx,也不是服务直接挂掉,而是“看起来成功了,但实际结果不完整”。

如果只盯着状态码,很容易误判成:

  • 服务端逻辑已经执行完了
  • 只是 Postman 显示有点问题
  • 或者接口偶发不稳定

但实际上,unexpected end of file 这种报错,往往意味着一件事:

响应体在传输或解压过程中,被提前截断了,客户端没有拿到一份完整的数据。

2. 第一步排查:先用 curl 验证是不是服务端问题

遇到这种问题,我第一反应不是改服务端代码,而是先做一件最简单但很关键的事:

1
curl -v '你的接口地址'

结果很重要:

  • curl 请求是正常的
  • 响应数据能完整返回

这一步基本可以先排除两类问题:

  1. 接口本身不是完全不可用
  2. 服务端并没有普遍性地返回坏数据

也就是说,这时候问题已经开始收敛了:

不是“接口一定有问题”,而是“某个客户端或者某条请求链路在处理这个响应时出了问题”。

这也是这次排查里最关键的转折点。

因为如果 curl 也失败,那排查方向应该优先放在:

  • 服务端程序
  • 网关
  • 上游接口
  • 数据库或超时

但既然 curl 成功,就说明我们更应该去对比“成功请求”和“失败请求”之间到底差在哪。

3. 第二步排查:为什么有些接口正常,有些接口不正常

继续看现象,会发现它不是“所有接口都挂了”,而是:

  • 有些接口在 Postman 里可以正常请求
  • 只有一部分接口会出现 unexpected end of file

这说明网络、域名、基本服务能力都不是全局异常,否则应该全部接口一起出问题。

所以这时候最合理的思路不是继续猜,而是做对比。

我这里重点对比了两类接口:

  1. 正常接口
  2. 异常接口

排查重点放在响应头上,尤其是下面几个字段:

  • Content-Type
  • Content-Length
  • Transfer-Encoding
  • Content-Encoding

结果很快就看到了一个非常关键的差异。

4. 关键线索:失败接口响应头里有 content-encoding: br

异常接口返回的响应头里,出现了下面这段信息:

1
2
3
content-encoding: br
content-type: application/json; charset=utf-8
server: cloudflare

其中最关键的是:

1
content-encoding: br

这里的 br 指的是 Brotli 压缩

也就是说,这个接口实际返回的不是裸 JSON,而是:

  1. 服务端生成 JSON
  2. 中间链路对响应做了 Brotli 压缩
  3. 客户端再去解压和解析

unexpected end of file 恰好非常像一种典型现象:

  • 压缩流没有被完整接收
  • 或者客户端在解压时拿到的是一段不完整的 Brotli 内容
  • 最终解压失败,报 EOF 类错误

到了这里,问题已经从“业务接口异常”收缩成了:

某些响应在 Brotli 压缩链路下,被 Postman 这一侧处理失败了。

5. 第三步排查:为什么 curl 能成功,Postman 却失败

这里就出现了一个很有价值的对照:

  • curl 能正常拿到响应
  • Postman 不行

这说明“响应不是绝对坏的”,而是“不同客户端对这份响应的处理结果不一样”。

进一步想,就有几个高概率方向:

  1. curl 和 Postman 发出的请求头不同
  2. 两者对压缩编码的支持和处理方式不同
  3. 某个代理或中间层会根据请求头返回不同编码方式

于是接下来做了一个很直接的验证:在请求头里主动限制压缩算法,不让服务端返回 Brotli。

例如:

1
Accept-Encoding: gzip, deflate

或者更激进一点:

1
Accept-Encoding: identity

结果很明确:

  • 当禁止 br 之后,请求恢复正常
  • 问题立刻消失

这一步其实已经足够说明:

真正触发问题的,不是接口业务逻辑本身,而是 Brotli 响应在当前测试链路中的兼容性问题。

6. 第四步排查:为什么测试环境有问题,生产环境没问题

如果问题只停留在“Postman 在当前 Brotli 响应下处理失败”,其实解释还不完整。

因为还有一个事实:

  • 测试环境有问题
  • 生产环境没有问题

继续往环境差异里看,发现一个非常关键的区别:

  • 测试环境机器上多了一层 Nginx
  • 生产环境没有这一层 Nginx 反向代理

这一下,排查方向就更明确了。

6.1. 当时的链路大概是什么样的

这次的请求链路可以近似理解成这样:

flowchart LR
    A[Postman / curl] --> B[域名入口]
    B --> C[Cloudflare]
    C --> D[Nginx 反向代理]
    D --> E[后端服务]
flowchart LR
    A[Postman / curl] --> B[域名入口]
    B --> C[Cloudflare]
    C --> D[Nginx 反向代理]
    D --> E[后端服务]
flowchart LR
    A[Postman / curl] --> B[域名入口]
    B --> C[Cloudflare]
    C --> D[Nginx 反向代理]
    D --> E[后端服务]

已知条件有几个:

  1. 域名入口支持 HTTP/2
  2. 测试环境前面多了一层 Nginx
  3. Nginx 代理到上游时,常见配置仍然是 HTTP/1.1
  4. 异常接口返回体较大,并触发了 Brotli 压缩

把这些条件拼起来,就能看出问题大概率发生在哪:

测试环境新增的 Nginx 反向代理,改变了请求和响应在链路中的传输方式,让 br 响应在这一条链上更容易出现兼容性问题。

6.2. 这里要注意一个容易说偏的点

这次排查里,最容易被一句话带偏的是:

“浏览器 / Postman 不支持 Brotli。”

这个说法并不严谨。

更准确地说应该是:

  • 浏览器本身通常是支持 Brotli 的
  • curl 在较新版本和正确编译参数下,也可能支持 Brotli
  • 真正出问题的是“当前测试环境这一整条链路下,Postman 对返回内容的处理失败了”

所以这次的根因不是“Brotli 这个技术本身有问题”,而是:

Brotli 响应叠加测试环境中的 Nginx 代理链路后,导致当前客户端请求出现了解压 / 接收不完整的问题。

7. 最终定位:问题不在接口代码,而在测试环境的代理链路

到这里,其实已经可以把问题定性了。

7.1. 现象层

  • Postman 状态码是 200
  • 但没有正常拿到响应体
  • unexpected end of file

7.2. 验证层

  • curl 正常
  • 限制 Accept-Encoding 后恢复正常
  • 只有部分接口出问题,且这些接口响应更大
  • 异常接口响应头里存在 content-encoding: br

7.3. 环境层

  • 测试环境有 Nginx
  • 生产环境没有 Nginx
  • 问题只在测试环境暴露

所以这次更准确的结论应该写成:

测试环境中的 Nginx 反向代理链路,叠加 Brotli 压缩响应,导致部分大响应接口在 Postman 中出现接收或解压异常,最终表现为状态码 200 但没有响应数据,并报 unexpected end of file

8. 最后是怎么处理的

既然已经确认问题和 Brotli 有关,那处理方式就很直接了:

8.1. 在 Nginx 代理层限制上游压缩算法

在测试环境的 Nginx 配置里,我在 location / 中增加了下面几行。为了避免暴露真实环境信息,下面用的是脱敏后的示例配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
    listen 80;
    server_name api-test.example.com;

    location / {
        proxy_pass http://upstream_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header Accept-Encoding "gzip, deflate";
        proxy_hide_header Upgrade;
        proxy_http_version 1.1;
    }
}

这里最关键的是这一行:

1
proxy_set_header Accept-Encoding "gzip, deflate";

它的作用是:

  • Nginx 在向后端发请求时,不再声明自己接受 br
  • 上游就不会优先返回 Brotli 压缩内容
  • 响应改成更稳定的 gzip / deflate
  • Postman 这边也就恢复正常了

8.2. 修改后结果

加完配置以后,问题就解决了:

  • Postman 能正常拿到响应体
  • 不再出现 unexpected end of file
  • 状态码和响应内容都恢复正常

这也进一步证明,前面的定位方向是对的。

9. 这次问题到底该怎么总结

如果用一句话概括这次问题,我会这样写:

这不是一次“接口业务报错”,而是一次“测试环境代理链路与 Brotli 压缩响应产生兼容性问题”的排查。

如果拆开来说,可以归纳成下面这条链:

  1. 域名入口支持 HTTP/2
  2. 测试服务器前面多了一层 Nginx
  3. Nginx 反向代理改变了请求链路
  4. 某些接口响应较大,命中了 Brotli 压缩
  5. 返回头里出现 content-encoding: br
  6. Postman 在当前链路下处理这类响应失败
  7. 最终表现为 200 但无响应体,并报 unexpected end of file
  8. 通过在 Nginx 层限制 Accept-Encoding,改用 gzip/deflate 后恢复正常

10. 这次排查里最有价值的几个经验

最后把这次最有价值的经验单独记一下。

10.1. 先用 curl 做对照测试

curl 能不能成功,往往能帮你非常快地把问题分成两类:

  • 服务端根本不通
  • 某个客户端 / 某条链路有问题

这一步特别省时间。

10.2. 遇到 200 但没数据,不要只盯状态码

状态码 200 只能说明“请求到达并被处理过”,不代表“响应体一定被完整、正确地接收和解析了”。

10.3. 响应头经常比响应体更重要

尤其是这些字段:

  • Content-Encoding
  • Content-Length
  • Transfer-Encoding
  • Content-Type

很多问题,答案其实就藏在头里。

10.4. 测试环境和生产环境的中间件差异,足够制造一类独立问题

这次生产没问题、测试有问题,本质就是因为测试环境多了一层 Nginx。

很多时候不是代码不一致,而是基础设施路径不一致。

11. 最后的结论

这次问题最终不是数据库、不是业务代码、不是接口返回值结构,也不是简单的 Postman 配置错误,而是:

测试环境中 Nginx 反向代理引入的链路差异,叠加部分大响应接口使用 Brotli 压缩,导致 Postman 在当前链路下无法正常处理响应体,最终报出 unexpected end of file

处理方式也不复杂:

  • 不去改业务接口
  • 不先怀疑服务端逻辑
  • 直接从代理层下手
  • 在 Nginx 中限制上游 Accept-Encoding,避免返回 br

问题就解决了。

如果以后你也碰到下面这种情况:

  • Postman 返回 200
  • 但没有响应数据
  • curl 正常
  • 只有部分接口异常
  • 响应头里有 content-encoding: br

那可以优先从 Brotli 压缩 + 代理链路兼容性 这个方向查起,命中率会很高。

0%