背景
- 使用springmvc实现一个通用网关代理
- 使用webClient作为请求被代理服务的http客户端
- 代码大致如下:
@RequestMapping("/proxy/{path}/**")
public void execute(@PathVariable("path") String path,
HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 准备请求, 得到webClient的WebClient.RequestBodyUriSpec proxyRequest
// 2. 处理一些业务逻辑, 确定被代理服务的请求地址
// 3. 执行请求
ResponseEntity<byte[]> proxyResponse = proxyRequest
.retrieve()
.toEntity(byte[].class)
.block();
// 4. 响应透传
WebProxyUtil.copyResponse(proxyResponse, response);
}
- 页面功能正常,但是在压测时会出现1%左右的异常,压测结果如下:
单独压下游未发现问题,所有这个异常确定是代理引入的。
排查过程
- 首先查看代理服务后台日志,未见异常
- 压测客户端报错:ChunkedEncodingError(ProtocolError('Connection broken: InvalidChunkLength(…), 0 bytes read)))
至此没有得到足以定位问题的信息,只能进一步wrieshark抓包来排查
- 通过抓包可以发现,在一次TCP连接的末尾出现异常包,该包的结构不符合http1.1对Transfer-Encoding:chunked的格式
- 这里的Transfer-Encoding:chunked,其实是http1.1引入的分块传输机制,适用于无法事先确定响应长度的情形。具体可以参考wiki 链接。其结构应该如下:
- 这里的Transfer-Encoding:chunked,其实是http1.1引入的分块传输机制,适用于无法事先确定响应长度的情形。具体可以参考wiki 链接。其结构应该如下:
从上面的排查可以推测,很可能是响应头设置的问题
- 来看看WebProxyUtil.copyResponse中响应头复制的相关代码
HttpHeaders httpHeaders = proxy.getHeaders();
// 写入响应头
for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
String headerName = entry.getKey();
List<String> headerValues = entry.getValue();
for (String headerValue : headerValues) {
origin.addHeader(headerName, headerValue);
}
}
可以看到响应头是全部复制的
原因分析
- tomcat生成响应的设计与实现可以参考: 链接
- 我们使用的tomcat-embeded-core的版本为10.1.8,对应的相关代码最终定位到:org.apache.coyote.http11.Http11Processor#prepareResponse
boolean connectionClosePresent = isConnectionToken(headers, Constants.CLOSE);
if {
// some code
} else {
// If the response code supports an entity body and we're on
// HTTP 1.1 then we chunk unless we have a Connection: close header
if (http11 && entityBody && !connectionClosePresent) {
outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]);
contentDelimitation = true;
headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
} else {
outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
}
}
- 可以看到上述代码是自动维护Connection与Transfer-Encoding这两个请求头的,没有过多考虑业务传入这两个请求头的请况。
- 尝试同时设置这两个请求头如下:
origin.addHeader(Constants.TRANSFERENCODING, Constants.CHUNKED);
origin.addHeader(Constants.CONNECTION, Constants.CLOSE);
这回所有请求都会报错了
总结
- http响应头不能随意透传,有些响应头是框架维护的,并且可以影响底层TCP连接。
- tomcat对http协议的处理逻辑在Http11Processor,后面可以进一步掌握与学习