J'utilise Retrofit 2.6.0 avec des coroutines pour mon appel de service Web. Je reçois correctement la réponse de l'API avec tous les codes de réponse (cas de réussite et d'erreur). Mon problème est que lorsque je déconnecte Internet (données Wifi/mobile) entre un appel API, à partir du code que j'ai écrit, l'erreur ne se rattrape pas correctement. La plupart du temps, les erreurs sont ConnectException et SocketException.
J'ai essayé de capturer l'erreur en utilisant l'intercepteur et également depuis le ViewModel où j'ai également initié mon appel. mais ici aussi, l'exception n'est pas prise et manipulée.
//ApiService
@GET(ApiUrl.API_DASHBOARD)
suspend fun getHomeUiDetails(@Header("Authorization") authHeader: String): Response<HomeDetailsResponse>
//ConnectionBridge
suspend fun getHomeUiDetails(authToken: String): Response<HomeDetailsResponse> {
return ApiServiceGenerator.BASIC_CLIENT_CONTRACT.getHomeUiDetails(authToken)
}
// ViewModel
viewModelScope.launch(Dispatchers.IO) {
val apiResponse = ApiConnectionBridge.getHomeUiDetails(SharedPrefUtils.getAuthToken(context))
if (apiResponse.isSuccessful) {
// success case
} else {
// error case
}
}
object ApiServiceGenerator {
val BASIC_CLIENT_CONTRACT: ApiService = ApiClient
.getContract(
ApiService::class.Java,
true,
BuildConfig.BASE_URL
)
}
object ApiClient {
fun <T> getContract(clazz: Class<T>, isAuth: Boolean, baseUrl: String): T {
return getRetrofitBuilder(baseUrl, getContractBuilder(isAuth)).create(clazz)
}
private fun getRetrofitBuilder(baseUrl: String, builder: OkHttpClient.Builder): Retrofit {
val gson = GsonBuilder().serializeNulls().create()
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
// Customize the request
val request = original.newBuilder()
request.header("Content-Type", "application/x-www-form-urlencoded")
var response: Response? = null
try {
response = chain.proceed(request.build())
response.cacheResponse()
// Customize or return the response
response!!
} catch (e: ConnectException) {
Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
chain.proceed(original)
} catch (e: SocketException) {
Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
chain.proceed(original)
} catch (e: IOException) {
Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
chain.proceed(original)
} catch (e: Exception) {
Log.e("RETROFIT", "ERROR : " + e.localizedMessage)
chain.proceed(original)
}
}
// .cache(cache)
.eventListener( object : EventListener() {
override fun callFailed(call: Call, ioe: IOException) {
super.callFailed(call, ioe)
}
})
.addInterceptor(loggingInterceptor)
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)//getUnsafeOkHttpClient()
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
}
La trace de la pile:
2019-08-02 14:15:12.819 4157-4288/com.my.app E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
Process: com.my.app, PID: 4157
Java.net.ConnectException: Failed to connect to my_url
at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.Java:248)
at okhttp3.internal.connection.RealConnection.connect(RealConnection.Java:166)
at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.Java:257)
at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.Java:135)
at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.Java:114)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.Java:42)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.Java:126)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.Java:213)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.Java:254)
at okhttp3.RealCall$AsyncCall.execute(RealCall.Java:200)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.Java:32)
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:764)
Caused by: Java.net.ConnectException: failed to connect to my_url (port 80) from /:: (port 0) after 60000ms: connect failed: ENETUNREACH (Network is unreachable)
at libcore.io.IoBridge.connect(IoBridge.Java:137)
at Java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.Java:137)
at Java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.Java:390)
at Java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.Java:230)
at Java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.Java:212)
at Java.net.SocksSocketImpl.connect(SocksSocketImpl.Java:436)
at Java.net.Socket.connect(Socket.Java:621)
at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.Java:73)
at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.Java:246)
at okhttp3.internal.connection.RealConnection.connect(RealConnection.Java:166)
at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.Java:257)
at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.Java:135)
at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.Java:114)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.Java:42)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.Java:126)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.Java:213)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.Java:254)
at okhttp3.RealCall$AsyncCall.execute(RealCall.Java:200)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.Java:32)
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:764)
Caused by: Android.system.ErrnoException: connect failed: ENETUNREACH (Network is unreachable)
at libcore.io.Linux.connect(Native Method)
at libcore.io.BlockGuardOs.connect(BlockGuardOs.Java:118)
at libcore.io.IoBridge.connectErrno(IoBridge.Java:168)
at libcore.io.IoBridge.connect(IoBridge.Java:129)
at Java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.Java:137)
at Java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.Java:390)
at Java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.Java:230)
at Java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.Java:212)
at Java.net.SocksSocketImpl.connect(SocksSocketImpl.Java:436)
at Java.net.Socket.connect(Socket.Java:621)
at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.Java:73)
at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.Java:246)
at okhttp3.internal.connection.RealConnection.connect(RealConnection.Java:166)
at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.Java:257)
at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.Java:135)
at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.Java:114)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.Java:42)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.Java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.Java:126)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.Java:213)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at com.my.app.network.ApiClient$getRetrofitBuilder$okHttpClient$1.intercept(ApiClient.kt:50)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:147)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.Java:121)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.Java:254)
at okhttp3.RealCall$AsyncCall.execute(RealCall.Java:200)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.Java:32)
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:764)
Eh bien, c'est ce que je fais, juste pour réduire le copypaste indésirable
Déclarez nos méthodes d'appel API comme ceci
@GET("do/smth")
suspend fun doSomething(): SomeCustomResponse
Dans un fichier séparé
suspend fun <T: Any> handleRequest(requestFunc: suspend () -> T): kotlin.Result<T> {
return try {
Result.success(requestFunc.invoke())
} catch (he: HttpException) {
Result.failure(he)
}
}
Usage:
suspend fun doSmth(): kotlin.Result<SomeCustomResponse> {
return handleRequest { myApi.doSomething() }
}
Les codes HTTP sont gérés par Retrofit - il lève juste une HttpException si responseCode n'est pas 2xx. Donc, ce que nous devons faire, c'est simplement attraper cette exception.
Je sais, ce n'est pas une solution parfaite, mais laissons Jake inventer quelque chose de mieux)
Vous pouvez simplement ajouter un CoroutineExceptionHandler
pour gérer l'erreur à votre place:
Dans votre ViewModel:
val coroutineExceptionHandler = CoroutineExceptionHandler{_, t -> {
t.printStackTrace()
showErrorOrSomething()
}}
viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val apiResponse = ApiConnectionBridge.getHomeUiDetails(SharedPrefUtils.getAuthToken(context))
if (apiResponse.isSuccessful) {
// success case
} else {
// error case
}
}
Peut-être que cela aide quelqu'un: Il est possible de se débarrasser de SocketTimeoutException
de la manière suivante: 1. Définissez le readTimeout de votre client sur un nombre arbitraire, ici ses 2
val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS).build()
2. Lorsque vous effectuez des appels API, enveloppez-les toujours dans un délai d'expiration coroutine.
try {
withTimeout(1000) {
try {
val retrivedTodo = APICall()
emit(retrivedTodo)
} catch (exception: HttpException) {
exception.printStackTrace()
}
}
}catch (ex: CancellationException) {
Log.e("timeout","TimeOut")
}
Le point principal est que la valeur withTimeout est inférieure à la valeur de délai d'expiration Retrofit. Cela garantit que la coroutine cesse d'être suspendue AVANT que le délai de mise à niveau ne se déclenche.
Quoi qu'il en soit, cela produit de nombreux blocs try/catch et n'est probablement pas ce que les développeurs de modernisation voulaient, en incluant le support de la coroutine.
Un ajout à la réponse d'Arthur Matsegor:
Dans mon cas, l'API me renvoie un message d'erreur pour les mauvaises demandes. Pour ce scénario, j'ai besoin d'attraper un message d'erreur sur la fonction Catch. Je sais, écrire la fonction try/catch dans Catch a l'air moche mais ça a marché.
private suspend fun <T : Any> handleRequest(requestFunc: suspend () -> T): Result<T> {
return try {
Result.success(requestFunc.invoke())
} catch (httpException: HttpException) {
val errorMessage = getErrorMessageFromGenericResponse(httpException)
if (errorMessage.isNullOrBlank()) {
Result.failure(httpException)
} else {
Result.failure(Throwable(errorMessage))
}
}
}
private fun getErrorMessageFromGenericResponse(httpException: HttpException): String? {
var errorMessage: String? = null
try {
val body = httpException.response()?.errorBody()
val adapter = Gson().getAdapter(GenericResponse::class.Java)
val errorParser = adapter.fromJson(body?.string())
errorMessage = errorParser.errorMessage?.get(0)
} catch (e: IOException) {
e.printStackTrace()
} finally {
return errorMessage
}
}