Differences of Suspend Functions with Retrofit on Exceptions

When our projects adopted coroutine and suspend functions with Retrofit v2.6 above, we found there are some differences between the response types and the root causes of why the error happened.

Let’s define two different responded types of Retrofit interface, one is ObjectService and it prefers the unmixed objects, and the other is ResponseService which returns retrofit2.Response.

interface ObjectService {
@GET("/")
suspend fun getString(): String?
}

interface ResponseService {
@GET("/")
suspend fun getString(): Response<String?>
}

And we write the unit tests to see what’s different.

@get:Rule
val server = MockWebServer()

For the ObjectService, when the API responses the error code of 400, suspend fun getString(): String? throws the exception.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
// This is because we didn't use Gson or others
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ObjectService::class.java)
server.enqueue(MockResponse().setResponseCode(400).setBody("Hi"))

try {
service.getString()
Assert.fail()
} catch (e: Exception) {
Assert.assertEquals("HTTP 400 Client Error", e.message)
}

Otherwise, when the ResponseService‘s API reponses the error code of 400, suspend fun getString(): Response<String?> won’t fire exceptions, instead, it will package the error code into retrofit2.Response.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ResponseService::class.java)
server.enqueue(MockResponse().setResponseCode(400).setBody("Hi"))

val result = service.getString()
Assert.assertEquals(400, result.code())
Assert.assertEquals(false, result.isSuccessful)
Assert.assertEquals(null, result.body())

However, suspend fun getString(): Response<String?> might still throw exceptions, for example, the SocketTimeoutException.

val retrofit = Retrofit.Builder()
.baseUrl(server.url("/"))
.addConverterFactory(StringConverterFactory())
.build()

val service = retrofit.create(ResponseService::class.java)
// we don't enque any reponse to emulate the timeout case

Since we needn’t respond code, so we use suspend fun getString(): String? to implement our all methods of Retrofit. To avoid uncaught exceptions, we use try-catch to wrap all the suspend methods.

However, it didn’t work, we still get java.net.SocketTimeoutException

Caused by java.net.SocketTimeoutException
failed to connect to xxxxxxx/xxx.xxx.xxx.xxx (port xxx) from /xxx.xxx.xxx.xxx (port xxx) after 10000ms

I will explain more details and the reason (we didn’t catch the exception) in the next story.

Lead Android & iOS Mobile Engineer at Yahoo (Verizon Media) Taiwan https://www.linkedin.com/in/hanruyeh/