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

Retrofit Call Adapter异常:偶现Result<Result<Exception>>返回问题排查

Kotlin Result与Retrofit Call Adapter偶现嵌套Result.Success<Result.Failure>问题排查与解决

问题描述

我实现了一个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协程适配器的自动包装逻辑冲突:

  1. Kotlin的Result是inline class,编译期会做特殊的类型擦除和包装优化。在高频并发的重试场景下,Retrofit的CoroutineCallAdapterFactory可能因为类型处理的竞态,误把我们手动包装的Result.Failure当成了泛型参数的成功值,从而二次包装成Result.Success。
  2. Retrofit对挂起函数有默认处理逻辑:不管是成功返回的响应体,还是抛出的异常,都会自动用Result包装。我们的Call Adapter已经把失败场景包装成Result.Failure并通过Response.success返回,此时Retrofit的协程适配器会把这个Result.Failure当作普通的成功响应体,再套一层Result.Success,最终形成嵌套结构。
  3. 高频重试时,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 01:59:56