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

如何为OkHttp数据流编写重试拦截器?流下载异常处理方案咨询

针对流式下载的OkHttp ECONNRESET异常处理方案

当然可行!不过普通的重试拦截器确实不太适配流式下载场景——毕竟流式下载已经开始读取响应体了,常规拦截器很难直接重试整个请求。不过我们可以通过自定义拦截器+断点续传逻辑,或者在请求执行的外层结合OkHttp的机制来搞定这类连接重置异常。

方案一:带断点续传的自定义拦截器

ECONNRESET本质是下载中途连接被重置,核心解决思路是让服务器从上次中断的位置继续传数据(也就是断点续传),这个逻辑可以封装在拦截器里:

  1. 拦截请求时,检查本地未完成的下载文件大小,给请求加上Range: bytes={已下载大小}-的请求头,告诉服务器从指定位置开始传输。
  2. 捕获到IOException(包括ECONNRESET)时,根据预设的重试次数,重新发起带Range头的请求。

下面是具体的代码实现:

public class ResumeDownloadInterceptor implements Interceptor {
    private final int maxRetryCount;
    private final DownloadProgressListener progressListener;

    public ResumeDownloadInterceptor(int maxRetryCount, DownloadProgressListener progressListener) {
        this.maxRetryCount = maxRetryCount;
        this.progressListener = progressListener;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        File downloadFile = new File(originalRequest.header("Download-File-Path"));
        long downloadedLength = downloadFile.exists() ? downloadFile.length() : 0;

        // 如果文件已经下载完成,直接返回成功响应
        if (downloadedLength > 0 && isFileComplete(downloadedLength, originalRequest)) {
            return new Response.Builder()
                    .request(originalRequest)
                    .protocol(Protocol.HTTP_1_1)
                    .code(HttpURLConnection.HTTP_OK)
                    .body(ResponseBody.create(MediaType.parse("application/octet-stream"), downloadFile))
                    .build();
        }

        // 构建带断点续传头的请求
        Request requestWithRange = originalRequest.newBuilder()
                .header("Range", "bytes=" + downloadedLength + "-")
                .build();

        int retryCount = 0;
        IOException lastException = null;

        while (retryCount <= maxRetryCount) {
            try {
                Response response = chain.proceed(requestWithRange);
                // 服务器返回206说明支持断点续传
                if (response.code() == HttpURLConnection.HTTP_PARTIAL) {
                    return new Response.Builder()
                            .request(response.request())
                            .protocol(response.protocol())
                            .code(response.code())
                            .message(response.message())
                            .headers(response.headers())
                            .body(new ResumableResponseBody(response.body(), downloadFile, progressListener))
                            .build();
                } else {
                    // 服务器不支持断点续传,删除已下载文件,从头开始
                    downloadedLength = 0;
                    downloadFile.delete();
                    requestWithRange = originalRequest.newBuilder().build();
                }
            } catch (IOException e) {
                lastException = e;
                retryCount++;
                progressListener.onRetry(retryCount, maxRetryCount);
                // 重试间隔递增,避免频繁请求服务器
                try {
                    Thread.sleep(1000 * retryCount);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new IOException("Retry interrupted", ie);
                }
            }
        }

        throw lastException != null ? lastException : new IOException("Max retries exceeded");
    }

    // 判断文件是否下载完成(需要对比服务器文件总大小)
    private boolean isFileComplete(long downloadedLength, Request request) {
        // 可以提前通过HEAD请求获取文件总大小,或者缓存到本地
        long totalLength = getTotalFileLength(request.url());
        return downloadedLength == totalLength;
    }

    // 通过HEAD请求获取文件总大小的实现
    private long getTotalFileLength(HttpUrl url) {
        OkHttpClient headClient = new OkHttpClient.Builder().build();
        Request headRequest = new Request.Builder().url(url).head().build();
        try (Response response = headClient.newCall(headRequest).execute()) {
            return response.body().contentLength();
        } catch (IOException e) {
            return -1;
        }
    }
}

// 自定义ResponseBody,处理流式写入和异常传递
class ResumableResponseBody extends ResponseBody {
    private final ResponseBody originalBody;
    private final File downloadFile;
    private final DownloadProgressListener progressListener;
    private BufferedSource bufferedSource;

    public ResumableResponseBody(ResponseBody originalBody, File downloadFile, DownloadProgressListener progressListener) {
        this.originalBody = originalBody;
        this.downloadFile = downloadFile;
        this.progressListener = progressListener;
    }

    @Override
    public MediaType contentType() {
        return originalBody.contentType();
    }

    @Override
    public long contentLength() {
        return originalBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(new ForwardingSource(originalBody.source()) {
                long bytesRead = 0;
                long downloadedLength = downloadFile.length();

                @Override
                public long read(Buffer sink, long byteCount) throws IOException {
                    try {
                        long read = super.read(sink, byteCount);
                        if (read != -1) {
                            bytesRead += read;
                            progressListener.onProgress(downloadedLength + bytesRead, downloadedLength + contentLength());
                            // 追加写入本地文件
                            try (FileOutputStream fos = new FileOutputStream(downloadFile, true)) {
                                sink.writeTo(fos);
                            }
                        }
                        return read;
                    } catch (IOException e) {
                        // 捕获流式读取中的ECONNRESET,抛给拦截器处理重试
                        throw e;
                    }
                }
            });
        }
        return bufferedSource;
    }
}

// 下载进度和重试监听接口
interface DownloadProgressListener {
    void onProgress(long current, long total);
    void onRetry(int currentRetry, int maxRetry);
}

方案二:在Call执行外层手动处理重试

如果觉得拦截器逻辑太复杂,也可以直接在调用OkHttp的Call时,在外层捕获异常并手动重试,结合断点续传:

public void downloadFile(String url, File saveFile, int maxRetry) {
    int retryCount = 0;
    boolean downloadSuccess = false;
    long downloadedLength = saveFile.exists() ? saveFile.length() : 0;

    while (retryCount <= maxRetry && !downloadSuccess) {
        OkHttpClient client = new OkHttpClient.Builder().build();
        Request request = new Request.Builder()
                .url(url)
                .header("Range", "bytes=" + downloadedLength + "-")
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                try (InputStream inputStream = response.body().byteStream();
                     FileOutputStream fos = new FileOutputStream(saveFile, true)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        fos.write(buffer, 0, bytesRead);
                        downloadedLength += bytesRead;
                        // 这里可以更新下载进度
                    }
                    downloadSuccess = true;
                }
            } else if (response.code() == HttpURLConnection.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
                // 已下载长度超过文件总长度,说明文件已经完成
                downloadSuccess = true;
            } else {
                // 遇到404等无法恢复的错误,直接终止
                break;
            }
        } catch (IOException e) {
            if (e.getMessage() != null && e.getMessage().contains("ECONNRESET")) {
                retryCount++;
                // 重试间隔递增
                try {
                    Thread.sleep(1000 * retryCount);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    break;
                }
            } else {
                // 其他IO异常(比如文件写入失败),根据业务情况处理
                break;
            }
        }
    }
}

重要注意事项

  • 服务器必须支持断点续传:上述方案的前提是服务器能识别Range请求并返回206状态码。如果服务器不支持,那只能从头重试,这时候要注意覆盖已下载的文件片段。
  • 合理设置重试策略:不要无限制重试,建议设置3-5次最大重试次数,并且每次重试间隔递增(比如1s、2s、3s),避免给服务器造成额外压力。
  • 文件完整性校验:下载完成后,一定要通过MD5/SHA等哈希值校验文件完整性,防止中途中断导致文件损坏。
  • 拦截器顺序:如果使用自定义拦截器,要注意添加到OkHttpClient的拦截器列表中,避免和日志拦截器等其他拦截器冲突。

总的来说,流式下载中的ECONNRESET问题不能用普通重试拦截器解决,但通过断点续传+拦截器/外层重试逻辑就能完美处理,核心是让服务器从上次中断的位置继续传输,而不是每次都从头开始下载。

内容的提问来源于stack exchange,提问作者blackkara

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 10:53:43