如何为OkHttp数据流编写重试拦截器?流下载异常处理方案咨询
针对流式下载的OkHttp ECONNRESET异常处理方案
当然可行!不过普通的重试拦截器确实不太适配流式下载场景——毕竟流式下载已经开始读取响应体了,常规拦截器很难直接重试整个请求。不过我们可以通过自定义拦截器+断点续传逻辑,或者在请求执行的外层结合OkHttp的机制来搞定这类连接重置异常。
方案一:带断点续传的自定义拦截器
ECONNRESET本质是下载中途连接被重置,核心解决思路是让服务器从上次中断的位置继续传数据(也就是断点续传),这个逻辑可以封装在拦截器里:
- 拦截请求时,检查本地未完成的下载文件大小,给请求加上
Range: bytes={已下载大小}-的请求头,告诉服务器从指定位置开始传输。 - 捕获到
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
相关产品推荐
相关产品推荐

