diff --git a/src/main/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/CoroutineCallAdapterFactory.kt b/src/main/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/CoroutineCallAdapterFactory.kt index 60771e6..0d0619e 100644 --- a/src/main/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/CoroutineCallAdapterFactory.kt +++ b/src/main/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/CoroutineCallAdapterFactory.kt @@ -92,8 +92,10 @@ class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() } } + val exceptionWithCapturedStack = CoroutineCallException() call.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { + (t.findRootCause() ?: t).initCause(exceptionWithCapturedStack) deferred.completeExceptionally(t) } @@ -101,7 +103,7 @@ class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() if (response.isSuccessful) { deferred.complete(response.body()!!) } else { - deferred.completeExceptionally(HttpException(response)) + deferred.completeExceptionally(HttpException(response).apply { initCause(exceptionWithCapturedStack) }) } } }) @@ -125,8 +127,10 @@ class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() } } + val exceptionWithCapturedStack = CoroutineCallException() call.enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { + (t.findRootCause() ?: t).initCause(exceptionWithCapturedStack) deferred.completeExceptionally(t) } @@ -138,4 +142,19 @@ class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() return deferred } } + } + +internal fun Throwable.findRootCause(): Throwable? { + var cause = cause ?: return null + + while (true) { + val nextCause = cause.cause + when (nextCause) { + null, cause -> return cause + else -> cause = nextCause + } + } +} + +private class CoroutineCallException : RuntimeException("Originally called here:") diff --git a/src/test/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/DeferredTest.kt b/src/test/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/DeferredTest.kt index 760622e..fa7a154 100644 --- a/src/test/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/DeferredTest.kt +++ b/src/test/java/com/jakewharton/retrofit2/adapter/kotlin/coroutines/DeferredTest.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy.DISCONNECT_AFTER_REQUEST +import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Before import org.junit.Rule @@ -60,23 +61,30 @@ class DeferredTest { @Test fun bodySuccess404() = runBlocking { server.enqueue(MockResponse().setResponseCode(404)) + val currentFrame = Exception().stackTrace[0] val deferred = service.body() try { deferred.await() fail() } catch (e: HttpException) { assertThat(e).hasMessageThat().isEqualTo("HTTP 404 Client Error") + + val rootCauseIsCurrentMethod = e.findRootCause()?.stackTrace?.any { it.equalsExceptLine(currentFrame) } ?: false + assertTrue("Root cause needs to be current method", rootCauseIsCurrentMethod) } } @Test fun bodyFailure() = runBlocking { server.enqueue(MockResponse().setSocketPolicy(DISCONNECT_AFTER_REQUEST)) + val currentFrame = Exception().stackTrace[0] val deferred = service.body() try { deferred.await() fail() } catch (e: IOException) { + val rootCauseIsCurrentMethod = e.findRootCause()?.stackTrace?.any { it.equalsExceptLine(currentFrame) } ?: false + assertTrue("Root cause needs to be current method", rootCauseIsCurrentMethod) } } @@ -101,11 +109,17 @@ class DeferredTest { @Test fun responseFailure() = runBlocking { server.enqueue(MockResponse().setSocketPolicy(DISCONNECT_AFTER_REQUEST)) + val currentFrame = Exception().stackTrace[0] val deferred = service.response() try { deferred.await() fail() } catch (e: IOException) { + val rootCauseIsCurrentMethod = e.findRootCause()?.stackTrace?.any { it.equalsExceptLine(currentFrame) } ?: false + assertTrue("Root cause needs to be current method", rootCauseIsCurrentMethod) } } + + private fun StackTraceElement.equalsExceptLine(other: StackTraceElement) = + other.className == className && other.fileName == fileName && other.methodName == methodName }