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

使用Retrofit抛出自定义IOException时如何保留请求调用栈?

解决Retrofit+OkHttp+Kotlin协程中自定义IOException子类栈追踪缺失问题

问题背景

使用Retrofit、OkHttp和Kotlin协程发起HTTP请求时,服务器返回非成功状态码(如404)会抛出继承自IOException的自定义异常,但异常栈追踪仅显示拦截器代码,无法定位到发起请求的原始调用位置(例如catApi.callThatReturns404())。

约束条件

  • 禁止为每个API调用添加try/catch,需全局统一处理
  • 仅靠拦截器日志不足以排查问题,必须保留完整栈追踪
  • 自定义异常不可弃用,其承载关键业务信息
  • 方案需在生产环境生效,不能仅限调试场景

示例代码

fun main(): Unit = runBlocking {
    makeApiCall()
}

private suspend fun makeApiCall() {
    val api = createCatApi()
    api.callThatReturns404()
}

private fun createCatApi(): CatApi {
    val url = "https://cat-fact.herokuapp.com/"
    val httpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val response = chain.proceed(chain.request())
            if (!response.isSuccessful) {
                throw SubTypeOfIOException(response)
            }
            response
        }
        .build()
    val retrofit = Retrofit.Builder()
        .baseUrl(url)
        .addConverterFactory(JacksonConverterFactory.create(ObjectMapper().registerKotlinModule()))
        .client(httpClient)
        .build()
    return retrofit.create<CatApi>()
}

interface CatApi {
    @GET("factss")
    suspend fun callThatReturns404(): Collection<Any>
}

当前栈追踪(异常)

Exception in thread "main" com.SubTypeOfIOException: 404
    at com.MainKt$createCatApi$$inlined$-addInterceptor$1.intercept(OkHttpClient.kt:1082)

期望栈追踪

Exception in thread "main" com.retrofit_test.SubTypeOfIOException: 404
    at com.retrofit_test.Main.makeApiCall(Main.kt:20)
    at com.MainKt$main$1.invokeSuspend(Main.kt:15)
    at com.MainKt.main(Main.kt:14)
Caused by: com.SubTypeOfIOException: 404
    at com.MainKt$createCatApi$$inlined$-addInterceptor$1.intercept(OkHttpClient.kt:1082)

关键现象

抛出原生IOException时,栈追踪会包含发起请求的调用位置;但抛出其自定义子类时,该部分信息缺失。

解决方案

方法1:通过Retrofit CallAdapter在协程上下文抛出异常

核心思路是不在OkHttp拦截器中直接抛出异常,而是将非成功响应返回给Retrofit,再通过自定义CallAdapter在API调用的协程上下文中抛出异常,确保栈追踪包含初始调用位置。

步骤1:修改拦截器,不再直接抛异常

val httpClient = OkHttpClient.Builder()
    .addInterceptor { chain ->
        chain.proceed(chain.request()) // 直接返回响应,后续由CallAdapter处理
    }
    .build()

步骤2:自定义CallAdapter处理异常

class SuspendErrorHandlingCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        val rawType = CallAdapter.Factory.getRawType(returnType)
        // 仅处理suspend函数(Retrofit会将suspend函数包装为Call<T>)
        if (rawType != Call::class.java) {
            val delegate = retrofit.nextCallAdapter(this, returnType, annotations)
            return object : CallAdapter<Any, Any> {
                override fun responseType(): Type = delegate.responseType()

                override fun adapt(call: Call<Any>): Any {
                    return suspend {
                        val response = delegate.adapt(call) as Response<Any>
                        if (!response.isSuccessful) {
                            val exception = SubTypeOfIOException(response)
                            // 将OkHttp执行栈作为异常原因保留
                            exception.initCause(Throwable("Request processed in OkHttp interceptor"))
                            throw exception
                        }
                        response.body() ?: throw IllegalStateException("Response body is null")
                    }
                }
            }
        }
        return null
    }
}

步骤3:为Retrofit添加自定义CallAdapter

val retrofit = Retrofit.Builder()
    .baseUrl(url)
    .addCallAdapterFactory(SuspendErrorHandlingCallAdapterFactory()) // 加入自定义CallAdapter
    .addConverterFactory(JacksonConverterFactory.create(ObjectMapper().registerKotlinModule()))
    .client(httpClient)
    .build()

方法2:手动合并栈追踪(适用于必须在拦截器抛异常的场景)

如果业务要求必须在拦截器中抛出异常,可以通过手动合并栈追踪的方式,将发起请求的调用栈附加到自定义异常中:

.addInterceptor { chain ->
    val response = chain.proceed(chain.request())
    if (!response.isSuccessful) {
        val exception = SubTypeOfIOException(response)
        // 获取当前线程的完整栈追踪(包含协程调用栈)
        val callerStackTrace = Throwable().stackTrace
        // 合并拦截器栈与调用者栈,去重避免重复帧
        exception.stackTrace = callerStackTrace + exception.stackTrace.filter { frame ->
            !callerStackTrace.any { it.className == frame.className && it.methodName == frame.methodName }
        }
        throw exception
    }
    response
}

版本信息

  • org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
  • com.squareup.retrofit2:retrofit:2.11.0
  • com.squareup.okhttp3:okhttp:4.12.0

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 05:05:08