1、问题现象
使用httpclient发送请求到外部web服务器(公网),目标地址网络通畅,刚开始请求也能发出去;一段时间后,请求报错,错误信息:
502 Bad Gateway
httpclient依赖如下
org.apache.httpcomponents
httpclient
4.5.6
restTemplate构造方式如下(省略部分代码):
@Bean
public RestTemplate restTemplate(RestTemplateProperties prop) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient(prop)));
}
private HttpClient httpClient(RestTemplateProperties prop) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
Registry registry = RegistryBuilder.create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", trustAllSslConnSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(prop.getMaxTotal());
connectionManager.setDefaultMaxPerRoute(prop.getDefaultMaxPerRoute());
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(prop.getSocketTimeout())
.setConnectTimeout(prop.getConnectTimeout())
.setConnectionRequestTimeout(prop.getConnectionRequestTimeout())
.build();
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.build();
}
private SSLConnectionSocketFactory trustAllSslConnSocketFactory() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
// 信任策略,信任全部
TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, trustStrategy).build();
return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
}
2、问题排查
生产抓包发现,请求头Accept-charset
与 cookie
特别大,如下:
尝试减少报文大小,可以成功发送;多次测试,得出目标服务器接收的报文大小是 4K
。
大于4K的报文超过了目标服务的处理能力,直接返回了 502 bad gaeway
。
报问题过大有两方面原因:
- cookie 过大
- accept-charset过大
cookie 过大
观察cookie内容,发现其中有阿里云相关的东西;猜测在网络链路上经过了阿里云的相关代理,返回了一些cookie,按照默认行为,http客户端第二次请求时会带上这些cookie,导致cookie越来越大。
accept-charset过大
经过代码排查,restTemplate会使用 StringHttpMessageConverter
作为其中的一个MessageConverter
;而这个Converter在转换请求消息时,有一个增加请求头-接收字符集的行为,如下:
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}
getAcceptedCharsets()
方法的实现如下:
protected List getAcceptedCharsets() {
List charsets = this.availableCharsets;
if (charsets == null) {
charsets = new ArrayList<>(Charset.availableCharsets().values());
this.availableCharsets = charsets;
}
return charsets;
}
端点跟踪发现,Charset.availableCharsets().values()
竟有 170个:
3、 问题解决
从问题原因看,解决方法是:
- 禁用 httpclient的cookie
- 禁止
StringHttpMessageConverter
写accept-charset
修改方式为:
1)构建HttpClient的时候,禁用cookie;
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(prop.getSocketTimeout())
.setConnectTimeout(prop.getConnectTimeout())
.setConnectionRequestTimeout(prop.getConnectionRequestTimeout())
.setCookieSpec(CookieSpecs.IGNORE_COOKIES) // 忽略cookie
.build();
2)禁止 StringHttpMessageConverter
写接受字符集;
@Bean
public RestTemplate restTemplate(RestTemplateProperties prop) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient(prop)));
List> messageConverters = restTemplate.getMessageConverters();
for (HttpMessageConverter> c : messageConverters) {
if (c instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) c).setWriteAcceptCharset(false);
}
}
return restTemplate;
}