Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception handling of network errors retrofit

I was wondering what is the best way to handle network errors in retrofit requests when using coroutines.

The classic way is handling exception at highest level, when a request is made:

try {
    // retrofit request
} catch(e: NetworkException) {
    // show some error message
}

I find this solution wrong and it adds a lot of boilerplate code, instead I went with creating an interceptor that returns a error response:

class ErrorResponse : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        return try {
            chain.proceed(request)
        } catch (e: Exception) {
            Snackbar.make(
                view,
                context.resources.getText(R.string.network_error),
                Snackbar.LENGTH_LONG
            ).show()
            Response.Builder()
                .request(request)
                .protocol(Protocol.HTTP_1_1)
                .code(599)
                .message(e.message!!)
                .body(ResponseBody.create(null, e.message!!))
                .build()
        }
    }
}

This solution is a little better, however I think that it can be improved.

So my question is: What is the correct way to handle the cases when user doesn't have internet connection, without a lot of boilerplate code (ideally with a global handler in case of connection errors) ?

like image 545
Daniel Avatar asked Oct 23 '19 12:10

Daniel


1 Answers

Using Result to wrap my response

sealed class Result<out T : Any> {
data class Success<out T : Any>(val value: T) : Result<T>()
data class Failure(val errorHolder:ErrorHolder) : Result<Nothing>()}

ErrorHolder :

sealed class ErrorHolder(override val message):Throwable(message){
 data class NetworkConnection(override val message: String) : ErrorHolder(message)
 data class BadRequest(override val message: String) : ErrorHolder(message)
 data class UnAuthorized(override val message: String) : ErrorHolder(message)
 data class InternalServerError(override val message: String) :ErrorHolder(message)
 data class ResourceNotFound(override val message: String) : ErrorHolder(message)
}

an extension to handle exeptions

suspend fun <T, R> Call<T>.awaitResult(map: (T) -> R): Result<R> = suspendCancellableCoroutine { continuation ->
try {
    enqueue(object : Callback<T> {
        override fun onFailure(call: Call<T>, throwable: Throwable) {
            errorHappened(throwable)
        }

        override fun onResponse(call: Call<T>, response: Response<T>) {
            if (response.isSuccessful) {
                try {
                    continuation.resume(Result.Success(map(response.body()!!)))
                } catch (throwable: Throwable) {
                    errorHappened(throwable)
                }
            } else {
                errorHappened(HttpException(response))
            }
        }

        private fun errorHappened(throwable: Throwable) {
            continuation.resume(Result.Failure(asNetworkException(throwable)))
        }
    })
} catch (throwable: Throwable) {
    continuation.resume(Result.Failure(asNetworkException(throwable)))
}

continuation.invokeOnCancellation {
    cancel()
}}

And this how I make the api call:

suspend fun fetchUsers(): Result<List<User>> {
    return service.getUsers().awaitResult { usersResponseDto ->
        usersResponseDto.toListOfUsers()
    }
}

UPDATE:

Let's say you have an error body like below:

{
  "error" : {
    "status" : 502,
    "message" : "Bad gateway."
  }
}

First we should create an data class to model response body

data class HttpErrorEntity(
@SerializedName("message") val errorMessage: String,
@SerializedName("status") val errorCode: Int
)

and here is asNetworkException implementation :

private fun asNetworkException(ex: Throwable): ErrorHolder {
    return when (ex) {
        is IOException -> {
            ErrorHolder.NetworkConnection(
                "No Internet Connection"
            )
        }
        is HttpException -> extractHttpExceptions(ex)
        else -> ErrorHolder.UnExpected("Something went wrong...")
    }
}

private fun extractHttpExceptions(ex: HttpException): ErrorHolder {
    val body = ex.response()?.errorBody()
    val gson = GsonBuilder().create()
    val responseBody= gson.fromJson(body.toString(), JsonObject::class.java)
    val errorEntity = gson.fromJson(responseBody, HttpErrorEntity::class.java)
    return when (errorEntity.errorCode) {
        ErrorCodes.BAD_REQUEST.code -> 
                ErrorHolder.BadRequest(errorEntity.errorMessage)
            
        ErrorCodes.INTERNAL_SERVER.code -> 
            ErrorHolder.InternalServerError(errorEntity.errorMessage)
        
        ErrorCodes.UNAUTHORIZED.code ->
            ErrorHolder.UnAuthorized(errorEntity.errorMessage)
       
        ErrorCodes.NOT_FOUND.code ->
            ErrorHolder.ResourceNotFound(errorEntity.errorMessage)
        
        else -> 
            ErrorHolder.Unknown(errorEntity.errorMessage)
        
    }
}
like image 108
Mohammad Sianaki Avatar answered Nov 04 '22 10:11

Mohammad Sianaki