记一次 UnzipError: incorrect header check...错误分析

今天前端小伙伴在调试前端项目遇到一个问题,简单分析记录下.

项目背景

  1. 前端项目基于Alibaba 的eggjs 构建的SSR项目.

  2. Api请求通过Nginx 反向代理到Gateway(Zuul v2.1.1RELEASE + nacos 1.0)

相关js

import { Service } from 'beidou';
export default class SimpleService extends Service {
async get(){
const result = await this.app.curl('http://service.demo.com/api/querySomething', {
method: 'GET',
dataType: 'json',
data: {id:1}
});
}
}


访问页面时NodeJs server 抛出如下异常

 { UnzipError: incorrect header check, GET http://service.demo.com/api/querySomething 200 (connected: true, keepalive socket: false, agent status: {"createSocketCount":1,"createSocketErrorCount":0,"closeSocketCount":0,"errorSocketCount":0,"timeoutSocketCount":0,"requestCount":0,"freeSockets":{},"sockets":{"testgate.feewee.cn:443::::::::::::::::":1},"requests":{}}, socketHandledRequests: 1, socketHandledResponses: 1)
headers: {"date":"Mon, 27 May 2019 01:52:46 GMT","content-type":"application/json;charset=UTF-8","transfer-encoding":"chunked","connection":"close","vary":"accept-encoding","content-encoding":"gzip","access-control-allow-origin":"*","access-control-allow-credentials":"true","access-control-allow-methods":"GET, POST, OPTIONS"}
at Zlib.zlibOnError [as onerror] (zlib.js:153:17)
errno: -3,
code: 'Z_DATA_ERROR',
name: 'UnzipError',
data:
<Buffer 7b 22 63 6f 64 65 22 3a 30 2c 22 72 65 73 75 6c 74 22 3a 22 e6 93 8d e4 bd 9c e6 88 90 e5 8a 9f 21 22 2c 22 64 61 74 61 22 3a 7b 22 61 63 74 69 76 69 ... >,
path:
'/api/querySomething?id=1',
status: 200,
headers:
{ date: 'Mon, 27 May 2019 01:52:46 GMT',
'content-type': 'application/json;charset=UTF-8',
'transfer-encoding': 'chunked',
connection: 'close',
vary: 'accept-encoding',
'content-encoding': 'gzip',
'access-control-allow-origin': '*',
'access-control-allow-credentials': 'true',
'access-control-allow-methods': 'GET, POST, OPTIONS' },
res:
{ status: 200,
statusCode: 200,
statusMessage: '',
headers:
{ date: 'Mon, 27 May 2019 01:52:46 GMT',
'content-type': 'application/json;charset=UTF-8',
'transfer-encoding': 'chunked',
connection: 'close',
vary: 'accept-encoding',
'content-encoding': 'gzip',
'access-control-allow-origin': '*',
'access-control-allow-credentials': 'true',
'access-control-allow-methods': 'GET, POST, OPTIONS' },
size: 528,
aborted: false,
rt: 433,
keepAliveSocket: false,
data:
<Buffer 7b 22 63 6f 64 65 22 3a 30 2c 22 72 65 73 75 6c 74 22 3a 22 e6 93 8d e4 bd 9c e6 88 90 e5 8a 9f 21 22 2c 22 64 61 74 61 22 3a 7b 22 61 63 74 69 76 69 ... >,
requestUrls: [ 'http://service.demo.com/api/querySomething' ],
timing: null,
remoteAddress: '192.168.0.251',
remotePort: 443,
socketHandledRequests: 1,
socketHandledResponses: 1 } }

上述异常是在请求/api/querySomething这个接口,处理响应内容时,出现gzip 解压错误.在浏览器里测试了下这个接口,接口是有正确返回。

分析

Gzip的文件头的前两个字节是1F 8B,上述响应头出现了content-encoding: gzip,表示内容使用了gzip压缩,但是上述接口返回的内容体 并不是gzip格式的。所以导致了gzip解压失败。

为了知道请求时具体发生了什么,使用java 模拟了下这个接口调用

public static void test2() throws IOException {
//为了防止HttpURLConnection 可能带来其他请求头的干扰,所以直接用socket 模拟 http 请求协议
Socket socket = new Socket("192.168.0.122",80);
String request =new String("GET /api/querySomething HTTP/1.1\r\n" +
"Host: service.demo.com\r\n" +
"Cache-Control: max-age=0\r\n" +
"DNT: 1\r\n" +
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\n" +
"Accept-Language: zh-CN,zh;q=0.9\r\n\r\n");
socket.getOutputStream().write(request.getBytes());
socket.getOutputStream().flush();
System.out.println(inputStreamToString2(socket.getInputStream()));
}

public static String inputStreamToString2(InputStream inputStream) throws IOException {
byte [] buffer = new byte[2048];
ByteArrayOutputStream os = new ByteArrayOutputStream();
int len = inputStream.read(buffer);
while(len != -1){
os.write(buffer, 0, len);
len = inputStream.read(buffer);
}
return new String(os.toByteArray());
}

测试输出结果如下

HTTP/1.1 200 OK
Date: Mon, 27 May 2019 14:48:32 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 103
Connection: keep-alive
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization,DNT,User-Agent,Keep-Alive,Content-Type,accept,origin,X-Requested-With
Access-Control-Allow-Methods: GET,OPTIONS,PUT,DELETE
Content-Encoding: gzip

{"code":1001,"result":"test result","data":null,"success":false}

再次测试,把请求头加上Accept-encoding:gzip

测试输出结果如下

HTTP/1.1 200 OK
Date: Mon, 27 May 2019 14:48:32 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 103
Connection: keep-alive
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization,DNT,User-Agent,Keep-Alive,Content-Type,accept,origin,X-Requested-With
Access-Control-Allow-Methods: GET,OPTIONS,PUT,DELETE
Content-Encoding: gzip

� I ��{"code":1001,"result":"test result","data":null,"success":false} �� ��UI

第一次请求返回的内容非gzip,第二次的请求返回内容已被gzip压缩过(上述乱码部分是 gzip 的相关标识字节序)

结论

  1. nginx server 每次的响应头都返回了 Content-Encoding: gzip

  2. 只对请求头中出现了Accept-encoding:gzip的请求才会压缩响应内容.

通过Wireshark 抓包发现,前端node server 发起的请求的头中没有 Accept-encoding:gzip .

capture

备注:

  1. nginx 已开启gzip
    gzip on;
  2. wireshark 过滤指定ip的数据包 ip.addr == target ip address

至于为啥请求头中没有Accept-encoding:gzip,nginx 还是返回 Content-Encoding: gzip ,只有研究下nginx的gzip模块源码才知道了(有机会补充)

!!! 上面的锅,Nginx 不背 !!!

补充(2019年5月28日)

研究了下发现,最终问题出在Zuul 网关。

分析Zuul网关源码发现,Zuul会把upstream服务器的响应头全部返回给客户端,即使内容非GZIP.

相关代码在 SendResponseFilter#addResponseHeader

这个类做了两件事

  1. 添加响应头

  2. 写响应内容

public void run() {
addResponseHeaders();
writeResponse();
}
//添加响应头
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
//....省略
//获取代理请求返回的响应头
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
if (zuulResponseHeaders != null) {
for (Pair<String, String> it : zuulResponseHeaders) {
///写入到ServletResponse中(实际返回给客户端)
servletResponse.addHeader(it.first(), it.second());
}
}
// .... 省略
}

private void writeResponse() throws Exception {
RequestContext context = RequestContext.getCurrentContext();

//...省略
HttpServletResponse servletResponse = context.getResponse();


OutputStream outStream = servletResponse.getOutputStream();
InputStream is = null;
try {
if (context.getResponseBody() != null) {
String body = context.getResponseBody();
is = new ByteArrayInputStream(
body.getBytes(servletResponse.getCharacterEncoding()));
}
else {
// 获取响应内容输入流
is = context.getResponseDataStream();
//判断是否响应内容是Gzip编码
if (is != null && context.getResponseGZipped()) {
// if origin response is gzipped, and client has not requested gzip,
// decompress stream before sending to client
// else, stream gzip directly to client
if (isGzipRequested(context)) {
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
}
else {
//问题就出在这里,虽然这里做了解压,但是没有去掉之前添加的"Content-encoding: gzip".
is = handleGzipStream(is);
}
}
}

if (is != null) {
writeResponse(is, outStream);
}
}

}

最新的master分支已经修复这个问题了.

相关Issue:Don’t set content-encoding header when un-gzipping

吐槽下:这个问题至最近才被人发现解决,去年在项目实际开发中就遇到了这个隐藏的bug(处理鹰眼围栏通知回调),迫于没有仔细研究,当时采用了其他方法解决,真是应了那句话 “天道好轮回,苍天饶过谁”

如何解决

  1. 前端在访问api时加上如下配置

    async get(){
    const result = await this.app.curl('http://service.demo.com/api/querySomething', {
    gzip: true,//当开启改选项时,请求头会自动添加`Accept-encoding:gzip`,默认关闭
    method: 'GET',
    dataType: 'json',
    data: {id:1}
    });
    }

    上述curl使用的js库为urllib

  2. 等spring-cloud-neflix 发布最新release,或者拉取最新的master分支源码本地install.

  3. 使用spring-cloud-gateway

参考

  1. GZIP压缩原理分析(04)——第三章 gzip文件格式详解(三02) gzip文件头