Retrofit crashes with UndeclaredThrowableException when using suspend modifier and making multiple requests which are supposed to throw IOException.
Retrofit version: 2.6.0, 2.6.1-SNAPSHOT (20190612)
class RetrofitUndeclaredThrowableExceptionBug {
interface Service {
@GET("/")
suspend fun download()
}
@Test
fun test() = runBlocking {
val retrofit = Retrofit.Builder()
.baseUrl("https://unresolved-host.com/") // An unresolved host to simulate IOException
.build()
val service = retrofit.create<Service>(Service::class.java)
// First attempt works fine
try {
service.download()
} catch (e: IOException) {
println("Catched 1: $e")
}
// Second attempt sometimes crashes
try {
service.download()
} catch (e: IOException) {
println("Catched 2: $e")
}
// And this will crash for sure (sometimes sooner, sometimes later)
repeat(Integer.MAX_VALUE) {
try {
service.download()
} catch (e: IOException) {
println("Catched 3: $e")
}
}
}
}
The same happens when throwing an exception from an interceptor
There should be a fat warning on the release notes, that using suspend modifier for retrofit is not production ready.
Network calls can throw checked IOExceptions so you need to declare @Throws(IOException::class) on every single function on your interface or it will blow up at runtime.
This happens when using _any_ non-wrapping call adapter, not just suspend. Despite the bytecode not caring about checked exceptions, they're verified at proxy boundaries for better or worse.
What's the difference between a wrapping and a non-wrapping adapter? I'd
like to test the difference in exceptions handling.
On Wed, Jul 24, 2019, 02:07 Jake Wharton notifications@github.com wrote:
This happens when using any non-wrapping call adapter, not just suspend.
Despite the bytecode not caring about checked exceptions, they're verified
at proxy boundaries for better or worse.—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=ABVG6BK3RQ2XAMY34L24YVTQA6MKVA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2UYUSA#issuecomment-514427464,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABVG6BLUZLY2PQBVKCG4XF3QA6MKVANCNFSM4HZCS6VA
.
There aren't any others in the library, but floating around you'll find various adapters which make API methods blocking and return the body/response object directly. They all have this restriction.
So, the safest way to use suspending functions in retrofit interfaces is to
add @Throws(Exception::class) to each of them?
I'm not sure what non-IOException Exception subtypes you would want to
recover from. Those seem indistinguishable from a runtime exception in
which case having them wrapped in a way similar to
InvocationTargetException doesn't seem like that big of a problem. Plus I'm
not convinced this can't be solved. Presumably the only case where this
occurs is when the exceptions are thrown synchronously (or delivered to the
continuation synchronously) which we just need to bounce into the
continuation on OkHttp's executor.
On Thu, Jul 25, 2019 at 6:37 PM Louis CAD notifications@github.com wrote:
So, the safest way to use suspending functions in retrofit interfaces is to
add@Throws(Exception::class)to each of them?—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=AAAQIENTSNWUABHPZLCNNZLQBITKHA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2267CA#issuecomment-515239816,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAQIEMIR54UW6LC6CQ3C4TQBITKHANCNFSM4HZCS6VA
.
I could want to recover from data format exceptions. Always better than
opaque unknown errors for the user and for troubleshooting.
On Fri, Jul 26, 2019, 00:44 Jake Wharton notifications@github.com wrote:
I'm not sure what non-IOException Exception subtypes you would want to
recover from. Those seem indistinguishable from a runtime exception in
which case having them wrapped in a way similar to
InvocationTargetException doesn't seem like that big of a problem. Plus I'm
not convinced this can't be solved. Presumably the only case where this
occurs is when the exceptions are thrown synchronously (or delivered to the
continuation synchronously) which we just need to bounce into the
continuation on OkHttp's executor.On Thu, Jul 25, 2019 at 6:37 PM Louis CAD notifications@github.com
wrote:So, the safest way to use suspending functions in retrofit interfaces is
to
add@Throws(Exception::class)to each of them?—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=AAAQIENTSNWUABHPZLCNNZLQBITKHA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD2267CA#issuecomment-515239816
,
or mute the thread
<
https://github.com/notifications/unsubscribe-auth/AAAQIEMIR54UW6LC6CQ3C4TQBITKHANCNFSM4HZCS6VA.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=ABVG6BLLIZZCVMQTWUPYWHTQBIUEZA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD227I6I#issuecomment-515241081,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABVG6BKCYRTGSWUTNNP74E3QBIUEZANCNFSM4HZCS6VA
.
Those are either unchecked or forced to be wrapped in unchecked at the
converter layer.
On Thu, Jul 25, 2019 at 6:57 PM Louis CAD notifications@github.com wrote:
I could want to recover from data format exceptions. Always better than
opaque unknown errors for the user and for troubleshooting.On Fri, Jul 26, 2019, 00:44 Jake Wharton notifications@github.com wrote:
I'm not sure what non-IOException Exception subtypes you would want to
recover from. Those seem indistinguishable from a runtime exception in
which case having them wrapped in a way similar to
InvocationTargetException doesn't seem like that big of a problem. Plus
I'm
not convinced this can't be solved. Presumably the only case where this
occurs is when the exceptions are thrown synchronously (or delivered to
the
continuation synchronously) which we just need to bounce into the
continuation on OkHttp's executor.On Thu, Jul 25, 2019 at 6:37 PM Louis CAD notifications@github.com
wrote:So, the safest way to use suspending functions in retrofit interfaces
is
to
add@Throws(Exception::class)to each of them?—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAAQIEMIR54UW6LC6CQ3C4TQBITKHANCNFSM4HZCS6VA
>.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=ABVG6BLLIZZCVMQTWUPYWHTQBIUEZA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD227I6I#issuecomment-515241081
,
or mute the thread
<
https://github.com/notifications/unsubscribe-auth/ABVG6BKCYRTGSWUTNNP74E3QBIUEZANCNFSM4HZCS6VA.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=AAAQIEPTPUJOSOWZLZ6K5FDQBIVWFA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD23ABSI#issuecomment-515244233,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAQIEK6WD7PNOHHUFBZL4TQBIVWFANCNFSM4HZCS6VA
.
Still happens with the final release of 2.6.1 (Samsung Galaxy Note9, Android 9)
Please providing a failing test case or reproducing sample.
On Wed, Aug 7, 2019 at 11:57 AM Paweł Marchewka notifications@github.com
wrote:
Still happens with the final release of 2.6.1 (Samsung Galaxy Note9,
Android 9)—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3128?email_source=notifications&email_token=AAAQIEKYDX5COLI2QWQ2T4LQDLWFVA5CNFSM4HZCS6VKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD3Y4A3Q#issuecomment-519159918,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAQIEOUWS4AHVGE45RI5M3QDLWFVANCNFSM4HZCS6VA
.
I'm still getting the UndeclaredThrowableExceptions within my app. Our Retrofit services return Responses instead of POJOs. My hunch (that I'm about to try and verify) is that you applied the new yieldAndThrow logic only to SuspendForBody but didn't apply the same logic to SuspendForResponse, and our services are using the currently unprotected SuspendForResponse
Yep! Whoops.
Executing requests in viewModelScope from androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha05 leads to the same Problem with the most basic setup that 2.6.1 should have fixed. Using viewModelScope from alpha03 works. It must have something to do with the move from Dispatchers.Main to Dispatchers.Main.immediate.
@JakeWharton Can you please re-open this ticket?
The following class crashes the application:
object UndeclaredThrowableBug {
interface Service {
@GET("/")
suspend fun download()
}
fun crash() {
GlobalScope.launch(Dispatchers.Main.immediate) {
val retrofit = Retrofit.Builder()
.baseUrl("https://unresolved-host.com/")
.build()
val service = retrofit.create<Service>()
while (true) {
try {
service.download()
} catch (ignored: IOException) {
}
}
}
}
}
java.lang.reflect.UndeclaredThrowableException
at $Proxy1.download(Unknown Source)
at com.yazio.android.UndeclaredThrowableBug$crash$1.invokeSuspend(App.kt:53)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:334)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: java.net.UnknownHostException: Unable to resolve host "unresolved-host.com": No address associated with hostname
at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:124)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103)
at java.net.InetAddress.getAllByName(InetAddress.java:1152)
at okhttp3.Dns$Companion$SYSTEM$1.lookup(Dns.kt:48)
at okhttp3.internal.connection.RouteSelector.resetNextInetSocketAddress(RouteSelector.kt:160)
at okhttp3.internal.connection.RouteSelector.nextProxy(RouteSelector.kt:125)
at okhttp3.internal.connection.RouteSelector.next(RouteSelector.kt:71)
at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:199)
at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:109)
at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:77)
at okhttp3.internal.connection.Transmitter.newExchange$okhttp(Transmitter.kt:162)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:35)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:82)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:84)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:71)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184)
at okhttp3.RealCall$AsyncCall.run(RealCall.kt:136)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
No. It's a coroutine bug: https://github.com/Kotlin/kotlinx.coroutines/issues/1474
Most helpful comment
The same happens when throwing an exception from an interceptor