使用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
相关产品推荐
相关产品推荐

