Retrofit Call Adapter异常:偶现Result<Result<Exception>>返回问题排查
问题描述
我实现了一个Retrofit Call Adapter,用来把请求响应包装成Kotlin官方的Result类:请求成功时返回Result.Success,失败(比如IOException)时返回携带自定义异常的Result.Failure。但在频繁重试请求的场景下,会偶现异常情况:明明触发了IOException,却收到了Result.Success<Result.Failure>,而非预期的Result.Failure。
测试发现:
- 间隔1000ms重试时不会复现
- 把Kotlin官方
Result换成自定义的Result类后,问题彻底消失 - 日志确认
NetworkResponseCall中callback.onResponse传入的Response对象结构正常,都是Response.success(Result.Failure)
相关核心代码如下:
interface UserEndpoint { @Headers( "Accept: application/json", "Content-Type: application/json", ) @GET("rest/x") suspend fun loadX(): Result<X> } internal class NetworkResponseCall<S : Any>( private val delegate: Call<S>, private val retrofit: Retrofit ) : Call<Result<S>> { override fun enqueue(callback: Callback<Result<S>>) { return delegate.enqueue(object : Callback<S> { override fun onResponse(call: Call<S>, response: Response<S>) { // 成功场景处理逻辑 } override fun onFailure(call: Call<S>, throwable: Throwable) { val networkResponse = when (throwable) { is IOException -> Result.failure<S>(NetworkException(throwable)) else -> Result.failure<S>(UnknownException(throwable)) } val response = Response.success(networkResponse) callback.onResponse(this@NetworkResponseCall, response) } }) } // 其他接口实现省略 } class ResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { if (Call::class.java != getRawType(returnType)) { return null } check(returnType is ParameterizedType) { "return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>" } val responseType = getParameterUpperBound(0, returnType) if (getRawType(responseType) != Result::class.java) { return null } check(responseType is ParameterizedType) { "Response must be parameterized as Result<Foo> or Result<out Foo>" } val successBodyType = getParameterUpperBound(0, responseType) return ResultCallAdapter<Any>(successBodyType, retrofit) } }
根因分析
这个问题的核心是Kotlin官方Result的inline class特性与Retrofit协程适配器的自动包装逻辑冲突:
- Kotlin的
Result是inline class,编译期会做特殊的类型擦除和包装优化。在高频并发的重试场景下,Retrofit的CoroutineCallAdapterFactory可能因为类型处理的竞态,误把我们手动包装的Result.Failure当成了泛型参数的成功值,从而二次包装成Result.Success。 - Retrofit对挂起函数有默认处理逻辑:不管是成功返回的响应体,还是抛出的异常,都会自动用
Result包装。我们的Call Adapter已经把失败场景包装成Result.Failure并通过Response.success返回,此时Retrofit的协程适配器会把这个Result.Failure当作普通的成功响应体,再套一层Result.Success,最终形成嵌套结构。 - 高频重试时,JVM的类型缓存或Retrofit的协程调度竞态放大了这个问题;而自定义Result类不是inline class,不会触发Retrofit协程适配器的特殊类型处理,因此没有问题。
解决方案
方案1:调整Call Adapter逻辑,规避Retrofit的二次包装
修改ResultCallAdapterFactory,直接处理挂起函数的Result<S>返回类型,确保我们的Adapter优先级高于Retrofit的协程适配器:
class ResultCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { // 直接匹配挂起函数的Result<S>返回类型 if (getRawType(returnType) != Result::class.java) { return null } check(returnType is ParameterizedType) { "Return type must be parameterized as Result<Foo>" } val successType = getParameterUpperBound(0, returnType) return object : CallAdapter<Any, Call<Result<Any>>> { override fun responseType(): Type = successType override fun adapt(call: Call<Any>): Call<Result<Any>> { return NetworkResponseCall(call, retrofit) } } } }
初始化Retrofit时,把ResultCallAdapterFactory放在CoroutineCallAdapterFactory之前:
Retrofit.Builder() .baseUrl(BASE_URL) .addCallAdapterFactory(ResultCallAdapterFactory()) // 优先级更高 .addCallAdapterFactory(CoroutineCallAdapterFactory()) .addConverterFactory(GsonConverterFactory.create()) .build()
方案2:改用自定义密封类Result
这是最稳妥的方案,完全规避Kotlin inline class带来的适配问题:
sealed class CustomResult<out T : Any> { data class Success<out T : Any>(val data: T) : CustomResult<T>() data class Failure(val exception: Exception) : CustomResult<Nothing>() }
把接口和Call Adapter中的所有Result替换为CustomResult即可,不需要调整其他逻辑。
方案3:修改失败处理逻辑,让Retrofit自动包装Result
放弃用Response.success返回Result.Failure,直接抛出异常,让Retrofit的协程适配器自动包装为Result.Failure:
override fun onFailure(call: Call<S>, throwable: Throwable) { val exception = when (throwable) { is IOException -> NetworkException(throwable) else -> UnknownException(throwable) } // 直接回调onFailure,让Retrofit处理异常包装 callback.onFailure(this@NetworkResponseCall, exception) }
此方案需要确保接口的挂起函数返回Result<S>时,Retrofit能正确捕获异常并包装。
验证
- 采用方案1或方案3后,高频重试场景下不再出现嵌套Result问题
- 方案2作为兜底方案,完全消除了inline class带来的适配风险
内容的提问来源于stack exchange,提问作者Qaltera

