You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 11:37:02