如何在HttpClient 5中遇TunnelRefusedException时重试请求?
问题描述
我希望HttpClient 5在遇到TunnelRefusedException时自动重试请求(该异常由代理随机拒绝初始连接导致,重试通常可解决)。
我曾尝试扩展DefaultHttpRequestRetryStrategy,但该策略仅能捕获继承自IOException的异常:
public class MyRetryStrategy extends DefaultHttpRequestRetryStrategy { public MyRetryStrategy() { super(2, TimeValue.ofSeconds(1L), Arrays.asList(InterruptedIOException.class, UnknownHostException.class, ConnectException.class, ConnectionClosedException.class, NoRouteToHostException.class, SSLException.class, TunnelRefusedException.class // 无效,因为它不是IOException! ), Arrays.asList(429, 503) ); } }
那么,是否应该让HttpRequestRetryStrategy接口支持所有类型的Exception?或者让TunnelRefusedException继承自IOException?
请问有什么可行的解决方法?
以下是需要重试的错误堆栈示例:
org.apache.hc.client5.http.ClientProtocolException: CONNECT refused by proxy: HTTP/1.1 500 Internal Server Error at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:173) at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:245) at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:188) [...] at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) at java.base/java.lang.Thread.run(Thread.java:1583) Caused by: org.apache.hc.client5.http.impl.TunnelRefusedException: CONNECT refused by proxy: HTTP/1.1 500 Internal Server Error at org.apache.hc.client5.http.impl.classic.ConnectExec.createTunnelToTarget(ConnectExec.java:284) at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:151) at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51) at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192) at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51) at org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:96) at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51) at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:152) at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51) at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:115) at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51) at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:170) ... 19 more
可行解决方法
1. 自定义HttpRequestRetryStrategy实现
直接实现HttpRequestRetryStrategy接口,绕过DefaultHttpRequestRetryStrategy的IO异常限制,手动检查异常链中的TunnelRefusedException:
import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.impl.TunnelRefusedException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.TimeValue; import java.io.IOException; import java.util.Arrays; import java.util.List; public class CustomRetryStrategy implements HttpRequestRetryStrategy { private static final List<Class<? extends IOException>> DEFAULT_RETRY_IO_EXCEPTIONS = Arrays.asList( InterruptedIOException.class, UnknownHostException.class, ConnectException.class, ConnectionClosedException.class, NoRouteToHostException.class, SSLException.class ); private static final List<Integer> DEFAULT_RETRY_STATUS_CODES = Arrays.asList(429, 503); private final int maxRetries; private final TimeValue retryInterval; public CustomRetryStrategy() { this.maxRetries = 2; this.retryInterval = TimeValue.ofSeconds(1L); } @Override public boolean retryRequest(HttpRequest request, IOException exception, int executionCount, HttpContext context) { if (executionCount >= maxRetries) { return false; } // 先检查默认需要重试的IO异常 for (Class<? extends IOException> clazz : DEFAULT_RETRY_IO_EXCEPTIONS) { if (clazz.isInstance(exception)) { return true; } } // 遍历异常链,检查是否包含TunnelRefusedException Throwable cause = exception.getCause(); while (cause != null) { if (cause instanceof TunnelRefusedException) { return true; } cause = cause.getCause(); } return false; } @Override public boolean retryRequest(HttpRequest request, int statusCode, HttpContext context) { return DEFAULT_RETRY_STATUS_CODES.contains(statusCode); } @Override public TimeValue getRetryInterval() { return retryInterval; } }
将自定义策略配置到HttpClient:
CloseableHttpClient client = HttpClients.custom() .setRetryStrategy(new CustomRetryStrategy()) .build();
2. 扩展DefaultHttpRequestRetryStrategy并覆盖重试逻辑
如果想复用父类的现有逻辑,可以继承DefaultHttpRequestRetryStrategy,然后覆盖retryRequest方法添加TunnelRefusedException的检查:
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy; import org.apache.hc.client5.http.impl.TunnelRefusedException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.util.TimeValue; import java.io.IOException; import java.util.Arrays; import java.util.List; public class MyRetryStrategy extends DefaultHttpRequestRetryStrategy { private static final List<Class<? extends IOException>> DEFAULT_RETRY_IO_EXCEPTIONS = Arrays.asList( InterruptedIOException.class, UnknownHostException.class, ConnectException.class, ConnectionClosedException.class, NoRouteToHostException.class, SSLException.class ); public MyRetryStrategy() { super(2, TimeValue.ofSeconds(1L), DEFAULT_RETRY_IO_EXCEPTIONS, Arrays.asList(429, 503) ); } @Override public boolean retryRequest(HttpRequest request, IOException exception, int executionCount, HttpContext context) { // 先调用父类的判断逻辑 boolean shouldRetry = super.retryRequest(request, exception, executionCount, context); if (shouldRetry) { return true; } // 检查异常链中是否存在TunnelRefusedException Throwable cause = exception.getCause(); while (cause != null) { if (cause instanceof TunnelRefusedException) { return executionCount < getMaxRetries(); } cause = cause.getCause(); } return false; } }
关于框架修改的疑问
- 让
HttpRequestRetryStrategy支持所有Exception类型:这属于HttpClient框架层面的变更,需要向Apache HttpClient团队提交需求评估,短期内无法自行实现。 - 让
TunnelRefusedException继承自IOException:同样属于框架内部代码修改,除非你愿意fork源码自行调整,否则不具备可行性。
综上,自定义重试策略检查异常链是最直接可行的解决方案。
内容的提问来源于stack exchange,提问作者Óscar
相关产品推荐
相关产品推荐

