基础环境
- SpringBoot:2.3.4.RELEASE
- 使用基于WebFlux的相关组件
- Java11
问题简述
在使用WebClient调用一个http地址时,总会抛出如下异常
[reactor-http-nio-1] HttpClientConnect |-> [id: 0x10fd540b, L:0.0.0.0/0.0.0.0:58092] The connection observed an error reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response
基础代码
/**
* 关于WebClient使用代理时的配置方式。更多细节请查看:
* https://projectreactor.io/docs/netty/release/api/reactor/netty/http/client/HttpClient.html
*/
@Test
public void webClientProxyTest() {
//说明:WebClientUtils为内部封装的工具类。这里使用访问对方站点(因为必须要使用代理)
WebClient client = WebClientUtils.getWebClientBuilderWithSslTrustAndPolicy(Duration.ofSeconds(100), new ProxyDO("xxxxxxxxxx", 100000L), true)
.build();
//流测试
StepVerifier.create(
client.get()
.uri("http://xxx/xxx")
.exchange().flatMap(v -> v.bodyToMono(String.class)))
.expectNextMatches(v -> {
System.out.println(v);
return StringUtils.isNotEmpty(v);
}).expectComplete().verify();
}
结论
为了节省大家的时间,先贴出结论O(∩_∩)O哈哈~
可以具体参看Github中关于issue#1442的相关表述和Commits:
Fix connection prematurely closed error
相关Bug已经被修复,但到文章发布时,暂时没有新的版本发布出来。建议不着急的小伙伴可以等待reactor-netty组件2021年第一个版本发布后,单独引入并升级
排查手记
接下来是排查相关的部分。贡献一些排查思路,供有兴趣的小伙伴参考
自定义WebClientBuilder
最开始怀疑是由于默认属性与目标服务器之间存在匹配问题,故尝试自定义生成WebClient。其中自定义了包含TcpConfiguration等的细节内容,但最终结论是无效~(或者没有尝试出有效组合)
当然,也顺带为项目写了个Utils,用于快捷定义一些包含超时时间等的参数。这里贡献一下
WebClientUtils
package com.aaa.common.utils;
import com.aaa.model.ProxyDO;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.ProxyProvider;
import java.net.InetSocketAddress;
import java.time.Duration;
/**
* WebClientUtils
*
* @author John Chen
* @since 2020/12/23
*/
public class WebClientUtils {
/**
* 默认3分钟超时时间
*/
private final static Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofMinutes(3L);
/**
* 默认代理超时时间
*/
private final static Long DEFAULT_PROXY_TIMEOUT_MILLIS = DEFAULT_REQUEST_TIMEOUT.toMillis();
//region 生成WebClient.Builder的方法
/**
* 给了一个默认的WebClient,这个Client里面配置了默认请求超时时间
*
* @return 返回一个带超时时间的{@link WebClient.Builder}
*/
public static WebClient.Builder getDefaultWebClientBuilder() {
return getWebClientBuilder(DEFAULT_REQUEST_TIMEOUT);
}
/**
* [基础创建方法]
* 给了一个默认的WebClient,这个Client里面配置了指定了请求超时时间
*
* @param requestTimeOut 请求超时时间
* @return 返回一个带超时时间的{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilder(Duration requestTimeOut) {
if (requestTimeOut == null) {
requestTimeOut = DEFAULT_REQUEST_TIMEOUT;
}
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient
.create()
//重新定向开启
.followRedirect(true)
.responseTimeout(requestTimeOut)));
}
/**
* 给到一个带默认超时时间,并带有不校验任何SSL整数的WebClient
*
* @return 返回一个带默认超时时间和默认全局信任的SSL请求校验器{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilderWithSslTrust() {
return getWebClientBuilderWithSslTrust(DEFAULT_REQUEST_TIMEOUT);
}
/**
* 给到一个带超时时间,并带有不校验任何SSL整数的WebClient
*
* @param requestTimeOut 超时时间
* @return 返回一个带超时时间和默认全局信任的SSL请求校验器{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilderWithSslTrust(Duration requestTimeOut) {
return getWebClientBuilderWithSslTrust(requestTimeOut, false);
}
/**
* [基础创建方法]
* 给到一个带超时时间,并带有不校验任何SSL整数的WebClient
*
* @param requestTimeOut 超时时间
* @param compressionEnabled 开启压缩?默认关闭
* @return 返回一个带超时时间和默认全局信任的SSL请求校验器{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilderWithSslTrust(Duration requestTimeOut, boolean compressionEnabled) {
if (requestTimeOut == null) {
requestTimeOut = DEFAULT_REQUEST_TIMEOUT;
}
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient
.create()
//重新定向开启
.followRedirect(true)
//这里注入了一个抛弃一切SSL认证的sslContext
.secure(sslContextSpec -> sslContextSpec.sslContext(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)))
.responseTimeout(requestTimeOut)
.compress(compressionEnabled)
));
}
/**
* 给到一个带超时时间,带代理,并带有不校验任何SSL整数的WebClient
*
* @param requestTimeOut 超时时间
* @param proxyDO 代理实体
* @return 返回一个带超时时间和默认全局信任的SSL请求校验器{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilderWithSslTrustAndPolicy(Duration requestTimeOut, ProxyDO proxyDO) {
return getWebClientBuilderWithSslTrustAndPolicy(requestTimeOut, proxyDO, false);
}
/**
* [基础创建方法]
* 给到一个带超时时间,带代理,并带有不校验任何SSL整数的WebClient
*
* @param requestTimeOut 超时时间
* @param proxyDO 代理实体
* @param compressionEnabled 开启压缩?默认关闭
* @return 返回一个带超时时间和默认全局信任的SSL请求校验器{@link WebClient.Builder}
*/
public static WebClient.Builder getWebClientBuilderWithSslTrustAndPolicy(Duration requestTimeOut, ProxyDO proxyDO, boolean compressionEnabled) {
if (requestTimeOut == null) {
requestTimeOut = DEFAULT_REQUEST_TIMEOUT;
}
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient
.create()
//这里注入了一个抛弃一切SSL认证的sslContext
.secure(sslContextSpec -> sslContextSpec.sslContext(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)))
.responseTimeout(requestTimeOut)
.compress(compressionEnabled)
//重新定向开启
.followRedirect(true)
.tcpConfiguration(tcpClient -> tcpClient.proxy(
p -> {
ProxyProvider.Builder pb = p.type(ProxyProvider.Proxy.HTTP)
.address(InetSocketAddress.createUnresolved(proxyDO.getServiceAddress(), Integer.parseInt(proxyDO.getPort())));
if (StringUtils.isNotEmpty(proxyDO.getUserName())) {
pb.username(proxyDO.getUserName())
.password(v -> proxyDO.getPassword());
}
Long proxyTimeOutMillis = proxyDO.getProxyTimeOutMillis();
if (proxyTimeOutMillis != null && proxyTimeOutMillis > 0) {
pb.connectTimeoutMillis(proxyTimeOutMillis);
} else {
pb.connectTimeoutMillis(DEFAULT_PROXY_TIMEOUT_MILLIS);
}
}
))
));
}
//endregion
}
百度
众里寻他千百度~首先还是找百度
其实在百度上,有许多关于关键字Connection prematurely closed BEFORE response的相关内容,但大多都跟Spring Cloud Gateway相关。但是笔者在这里所用的并不涉及到这个组件。
其中比较有代表性的一篇文章可以参看reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response解决方案
受上述文章启发,笔者转向了Github去寻找答案
Github
来到了reactor-netty项目的issues页面
能看到已经有人提出了相关问题
其实在之前我已经多次经过这个页面了,直到昨天,找到了一条7天前更新的消息,引起了我的主意
进入这个RP后,可以看到有一个ISSUE已经被Closed了
而进入后,则为我们结论部分所提到的相关Issue
Fix connection prematurely closed error
至此,问题排查结束。
由于笔者已经用其它的Http组件暂时解决当前需求,所以暂不着急。坐等组件更新后,单独引入修复当前问题。