Retrofit 2.6.0とコルーチンを使用して、Webサービスを呼び出しています。すべての応答コード(成功およびエラーの場合)でAPI応答を適切に取得しています。私の問題は、API呼び出しの間にインターネット(Wifi /モバイルデータ)を切断すると、作成したコードからエラーが正しくキャッチされないことです。ほとんどの場合、エラーはConnectExceptionとSocketExceptionです。
私はインターセプターを使用して、また私の呼び出しを開始したViewModelからもエラーをキャッチしようとしました。しかし、ここでも、例外は捕捉されて処理されていません。
//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()
}
}
スタックトレース:
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)
まあ、それは私がやっていることです、トライキャッチキャッチジャンクコピーペーストを減らすためだけに
このようなAPI呼び出しメソッドを宣言します
@GET("do/smth")
suspend fun doSomething(): SomeCustomResponse
別のファイル
suspend fun <T: Any> handleRequest(requestFunc: suspend () -> T): kotlin.Result<T> {
return try {
Result.success(requestFunc.invoke())
} catch (he: HttpException) {
Result.failure(he)
}
}
使用法:
suspend fun doSmth(): kotlin.Result<SomeCustomResponse> {
return handleRequest { myApi.doSomething() }
}
HTTPコードはRetrofitによって処理されます-responseCodeが2xxでない場合、HttpExceptionをスローするだけです。したがって、私たちがすべきことは、この例外をキャッチすることです。
私は知っています、それは完璧な解決策ではありませんが、ジェイクにもっと良いものを発明させましょう
CoroutineExceptionHandler
を追加するだけで、エラーを処理できます。
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
}
}
多分それは誰かを助ける:次の方法でSocketTimeoutException
を取り除くことが可能です:1.クライアントのreadTimeoutを任意の数に設定します。ここでは2sです
val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS).build()
2. API呼び出しを行うときは、常にコルーチンタイムアウト内にラップします。
try {
withTimeout(1000) {
try {
val retrivedTodo = APICall()
emit(retrivedTodo)
} catch (exception: HttpException) {
exception.printStackTrace()
}
}
}catch (ex: CancellationException) {
Log.e("timeout","TimeOut")
}
重要な点は、withTimeout値がRetrofitタイムアウト値より小さいことです。これにより、レトロフィットタイムアウトが発生する前に、コルーチンの中断が停止されます。
とにかく、これは多くのtry/catchブロックを生成しますが、コルーチンのサポートを含めると、レトロフィットの開発者が望んだものではないでしょう。
Arthur Matsegorの回答への追加:
私の場合、APIは不正なリクエストに対してエラーメッセージを返します。このシナリオでは、Catch関数でエラーメッセージをキャッチする必要があります。 Catch関数にtry/catchを書くのは醜いように見えますが、うまくいきました。
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
}
}