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

