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

Retrofit2上传图片抛出ProtocolException的问题及解决方法咨询

解决Retrofit2弱网下图片上传的ProtocolException问题

我来帮你拆解下这个问题哈,弱网环境下的文件上传问题确实挺常见的,咱们一步步来解决:

首先你遇到的java.net.ProtocolException: expected x bytes but received y异常,本质是弱网下请求体传输中断,OkHttp(Retrofit的底层依赖)校验实际接收字节数和预先计算的文件长度不匹配导致的。下面分几个维度给你解决方案:

一、先修复这个异常的直接办法

你现在用的是直接通过File创建RequestBody的方式:

val body = RequestBody.create(MediaType.parse("image/jpg"), image)

这种方式下OkHttp会提前读取文件的总长度,传输过程中如果网络波动导致部分字节丢失,就会触发这个校验异常。可以改成流式RequestBody,让OkHttp不提前计算总长度,而是边读边传:

val body = object : RequestBody() {
    override fun contentType(): MediaType? {
        return MediaType.parse("image/jpg")
    }

    override fun writeTo(sink: BufferedSink) {
        FileInputStream(image).use { inputStream ->
            sink.writeAll(inputStream.source())
        }
    }
}

这种方式不会预先计算文件长度,也就避免了长度校验不匹配的问题,特别适合弱网环境。

另外,记得给OkHttpClient设置合理的超时时间,避免因为网络过慢导致提前断开:

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .writeTimeout(60, TimeUnit.SECONDS) // 上传场景重点加这个写超时
    .build()

// 把这个自定义client传给Retrofit
val retrofit = Retrofit.Builder()
    .client(okHttpClient)
    // ...其他baseUrl、Converter配置
    .build()

二、是否需要重试逻辑?非常有必要!

弱网下重试是提升上传成功率的核心手段,你可以用两种方式实现:

1. RxJava的retry操作符(适配你当前的调用链)

直接在RxJava调用链里添加重试逻辑,可以设置重试次数,甚至只在特定异常时重试:

ApiService.create().uploadImage(body)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribeOn(Schedulers.io())
    // 简单版:最多重试3次
    .retry(3)
    // 智能版:只在ProtocolException时重试,且不超过3次
    // .retry { attempt, error ->
    //     attempt < 3 && error is ProtocolException
    // }
    .subscribe({ result -> 
        Log.d("NoError", result.toString()) 
    }, { error -> 
        Log.d("FatalError", error.toString()) 
    }, {} )

2. OkHttp全局重试Interceptor

如果想让所有请求都具备重试能力,可以自定义Interceptor:

class RetryInterceptor(private val maxRetry: Int) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var response: Response? = null
        var retryCount = 0
        var lastException: IOException? = null

        while (retryCount < maxRetry) {
            try {
                response = chain.proceed(request)
                if (response.isSuccessful) {
                    return response
                }
            } catch (e: IOException) {
                lastException = e
            }
            retryCount++
        }
        throw lastException ?: IOException("Retry failed after $maxRetry attempts")
    }
}

// 添加到OkHttpClient
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor(3))
    // ...其他配置
    .build()

三、Retrofit2其他的文件/图片上传方式

除了你当前用的单RequestBody上传,还有两种更常用的方式:

1. MultipartBody上传(推荐,支持多文件+参数)

如果需要同时上传图片和其他业务参数(比如用户ID、图片描述),用MultipartBody是更规范的做法:

// 先定义ApiService接口
interface ApiService {
    @Multipart
    @POST("upload")
    fun uploadImage(
        @Part image: MultipartBody.Part,
        @Part("userId") userId: RequestBody
    ): Observable<UploadResult>
}

// 调用时构建请求体
val requestFile = RequestBody.create(MediaType.parse("image/jpg"), image)
val imagePart = MultipartBody.Part.createFormData("image", image.name, requestFile)
val userIdBody = RequestBody.create(MediaType.parse("text/plain"), "user_123")

ApiService.create().uploadImage(imagePart, userIdBody)
    // ...后续RxJava调用逻辑

2. 分块上传(适合大图片)

如果图片体积很大,弱网下一次性上传容易失败,可以实现分块上传:把图片切成多个小片段,逐个上传,最后在服务器端合并成完整文件。这种方式需要服务器端配合实现分块合并的逻辑,Retrofit端就是多次调用上传接口,每次传递片段字节、片段索引、总片段数等参数。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:53:38