问题现象:再国产麒麟系统部署vue前端和java后端之后,由于前端业务问题需要使用nginx进行代理,在运行一段时间之后,接口突然提示响应失败,浏览器 Network 面板显示状态码却仍为200,但前端提示请求失败,部分较大的图片也存在只显示一半或加载失败的现象。通过查阅资料和一些官方文档得以解决。

一、运行环境与故障现象

故障环境如下:

  • 操作系统:国产麒麟 Linux
  • Web 服务器:Nginx 1.29.1
  • 前端:Vue
  • 后端 SpringBoot jar包
  • 浏览器客户端:Chrome
  • 访问方式:HTTPS
  • 后端接口:通过 Nginx_proxy_pass 反向代理

现象/截图:

  1. 小数据量 JSON 可以正常返回。
  2. 大数据量 JSON 会在某个字段中间结束,缺少后续数组元素和闭合括号。
  3. 前端通常表现为JSON.parse失败、Axios 请求失败。
  4. 截断位置并不是固定的 200 KB,如接口完整响应约为 500 KB,而浏览器只接收到约 300 KB。
  5. 某些通过代理访问的大图片也可能只显示一部分。

返回数据不是合法 JSON,响应体在传输过程中提前结束。

二、为什么响应被截断,返回状态仍是200

HTTP 响应分为响应头和响应体。服务器通常先发送响应头:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked

浏览器收到响应头后,就会在 Network 面板中显示200。如果后续传输响应体时连接中断,已经发出的状态码无法再改成 500,所以会出现“状态码 200,但 JSON 不完整”的情况。因此,200状态只能说明响应头成功发送,不能证明整个响应体已经完整到达浏览器。

查询诸多资料发现,反向代理场景至少包含两端独立连接:

浏览器 --HTTP/1.1--> Nginx --HTTP/1.0--> 后端服务

浏览器开发者工具只能直接看到第一段连接,即使状态正常,也不能证明 Nginx 请求后端时使用的是 HTTP/1.1。Nginx 完全可能从后端接收 HTTP/1.0 响应,再转换成 HTTP/1.1 chunked 响应发送给浏览器,实际使用的上游协议需要查看 Nginx 配置:

nginx -T | grep -n "proxy_http_version"

三、http1.0与http1.1的响应界定差异

HTTP 客户端必须知道响应体在哪里结束,常见方式有三种。

Content-Length

服务器提前计算响应体长度,客户端读取指定字节数。如果连接提前关闭,客户端可以判断还有数据未收到。

HTTP/1.1 chunked 分块传输

动态接口往往无法提前知道总长度,因此可以按块发送:
Transfer-Encoding: chunked

每个块都带有长度,最后通过 0 长度块明确表示响应完成。连接是否关闭不再是“标记响应结束”的主要职责。

通过关闭连接表示响应结束

HTTP/1.0 不支持标准的 chunked 传输。动态响应没有 Content-Length 时,通常只能发送完数据后关闭连接,以 EOF 表示响应结束,问题在于“正常发送完成后关闭连接”、“响应只发送一部分,连接异常断开”。

在仅依赖连接关闭定界时非常相似。如果后端应用服务器、连接池或代理层在 HTTP/1.0 模式下提前关闭连接,Nginx可能已经向浏览器返回了200,最终浏览器只能得到一个被截断的响应体。

四、为什么小响应正常,大响应容易失败

研究得出小响应可能一次写操作就能完成:

写JSON -> 写Websocket -> 关闭连接

大响应通常需要多次写入,并经过更多缓冲和调度:

生成一部分 -> 写入
生成下一部分 -> 再写入
多次循环 -> 完成

响应越大,越容易触发连接关闭、输出流、缓冲区切换或连接池中的兼容问题。因此数据量看起来像触发条件,但并不存在一个固定的“返回数据大小限制”。

五、测试流程

1.在本地使用windows客户端curl 复现

在 Chrome Network 中右键请求,选择“复制 -> 以 cURL 格式复制(CMD)”,需要注意Windows CMD 不能使用 Linux 风格的单引号,参数必须使用英文半角双引号。若内网证书无法检查吊销状态,Windows curl 可能报:

curl: (35) schannel: InitializeSecurityContext failed: 0x80092012

诊断是可以添加:

-k --ssl-no-revoke

测试关闭长连接:

curl.exe -k --ssl-no-revoke -v --http1.1 "https://x.x.x.x:xxxx/baseApi/task/query" -H "Accept: application/json" -H "Content-Type: application/json" -H "Connection: close" --data-raw "{\"id\":29}" --output result-close.json

echo curl退出码=%errorlevel%
dir result-close.json

测试保持长连接:

curl.exe -k --ssl-no-revoke -v --http1.1 "https://x.x.x.x:xxxx/baseApi/task/query" -H "Accept: application/json" -H "Content-Type: application/json" -H "Connection: keep-alive" --data-raw "{\"id\":29}" --output result-keepalive.json

echo curl退出码=%errorlevel%
dir result-keepalive.json

验证返回的 JSON 是否完整:

powershell -NoProfile -Command "(Get-Item .\result-close.json).Length"
powershell -NoProfile -Command "Get-Content -Raw .\result-close.json | ConvertFrom-Json | Out-Null; Write-Host 'JSON完整'"

常见 curl 结果:

退出码 0:传输正常完成
退出码 18:实际收到的数据少于协议声明的数据
退出码 35:TLS握手或证书问题
退出码 56:连接被重置或接收失败

2.在麒麟服务器本机测试 Nginx

我的服务器注记依赖Host/SNI,测试命令如下:

curl -k -v --http1.1 \
  --resolve x.x.x.x:xxxx:127.0.0.1 \
  'https://x.x.x.x:xxxx/baseApi/task/query' \
  -H 'Content-Type: application/json' \
  -H 'Connection: close' \
  --data-raw '{"id":29}' \
  -o /tmp/result-nginx.json

echo "curl exit=$?"
wc -c /tmp/result-nginx.json
tail -c 300 /tmp/result-nginx.json

jq可以验证 JSON:

jq empty /tmp/result-nginx.json && echo 'JSON完整'

3.绕过 Nginx,直接对比后端 HTTP/1.0 和 HTTP/1.1

这是最能验证根因的测试。将地址和端口替换成真实后端地址:

curl -v --http1.0 \
'http://x.x.x.x:xxxx/task/query' \
-H 'Content-Type: application/json' \
--data-raw '{"id":29}' \
-o /tmp/backend-http10.json

curl -v --http1.1 \
'http://x.x.x.x:xxxx/task/query' \
-H 'Content-Type: application/json' \
--data-raw '{"id":29}' \
-o /tmp/backend-http11.json

wc -c /tmp/backend-http10.json /tmp/backend-http11.json
jq empty /tmp/backend-http10.json
jq empty /tmp/backend-http11.json

如果 HTTP/1.0 结果被截断而 HTTP/1.1 完整,就直接证明问题发生在 Nginx与后端的协议或连接处理层。

5. 检查麒麟系统上的 Nginx 日志和运行环境

复现时观察:

tail -f /var/log/nginx/error.log
journalctl -u nginx -f

麒麟系统也必须遵循相同的 HTTP 规范。操作系统本身一般不会主动限制 返回数据 响应,但发行版内核、OpenSSL版本、Nginx编译参数、安全模块和防火墙会影响具体表现,因此排查时注意这些环境信息。

六、最终nginx配置

1.普通 HTTP/JSON 接口配置:

location /baseApi/ {
    proxy_pass http://x.x.x.x:xxxx;

    # Nginx 到后端使用 HTTP/1.1
    proxy_http_version 1.1;

    # 不将客户端的逐跳 Connection 头传递给后端
    proxy_set_header Connection "";

    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 X-Forwarded-Proto $scheme;

    proxy_connect_timeout 30s;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

重新加载:

nginx -t
systemctl reload nginx

2.WebSocket 配置

需要显式升级:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

七、错误修改和结论

找问题的时候改过"client_max_body_size、client_body_buffer_size",但是它们主要控制客户端上传的请求体,而本次问题发生在服务器返回的响应体,所以改了没什么用,“proxy_buffer_size”和“proxy_buffers”,也不是接口响应的最大大小。响应超过内存缓冲后,Nginx通常会使用代理临时文件;只有临时目录权限、磁盘空间或 I/O 异常时,才可能造成响应失败。

本次问题的表面现象是“大 JSON 被截断但状态码仍为 200”,真正需要检查的是反向代理两侧的协议和响应边界。

最终修复配置为:

proxy_http_version 1.1;
proxy_set_header Connection "";

修复后,大数据 JSON 可以完整返回,浏览器不再报请求失败。关于这次排查得到的几个关键经验:

  • 返回状态200不代表响应体完整。
  • 浏览器显示 HTTP/1.1,不代表 Nginx 到后端也是 HTTP/1.1。
  • HTTP/1.0 没有小响应大小限制,问题在于当前环境中的响应定界和连接处理兼容性。
  • 大响应更容易暴露多次写入、连接关闭和代理协议问题。
  • 最有效的定位方法是分别测试浏览器、Nginx本机和后端直连,并对比 HTTP/1.0 与 HTTP/1.1。

参考资料: