Skip to content

Instantly share code, notes, and snippets.

@timusus
Last active March 24, 2023 16:10
Show Gist options
  • Save timusus/df8d69df245759060acf0ad2f49a8872 to your computer and use it in GitHub Desktop.
Save timusus/df8d69df245759060acf0ad2f49a8872 to your computer and use it in GitHub Desktop.
Retrofit Error Handling
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.util.concurrent.Executor
class CallResultAdapterFactory(private val responseErrorMapper: ((Response<*>) -> Error?)? = null) : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (CallAdapter.Factory.getRawType(returnType) != CallResult::class.java) {
return null
}
if (returnType !is ParameterizedType) {
throw IllegalStateException("CallResult must have generic type (e.g., CallResult<ResponseBody>)")
}
val responseType = CallAdapter.Factory.getParameterUpperBound(0, returnType)
val callbackExecutor = retrofit.callbackExecutor()
return CallResultAdapter<Any>(responseType, callbackExecutor, responseErrorMapper)
}
private class CallResultAdapter<R> constructor(
private val responseType: Type,
private val callbackExecutor: Executor?,
private val responseErrorMapper: ((Response<*>) -> Error?)? = null
) : CallAdapter<R, CallResult<R>> {
override fun responseType(): Type {
return responseType
}
override fun adapt(call: Call<R>): CallResult<R> {
return CallResultMapper(call, callbackExecutor, responseErrorMapper)
}
}
}
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.util.concurrent.Executor
sealed class Result<out T> {
data class Success<out T>(val data: T?) : Result<T>()
data class Failure(val error: Error) : Result<Nothing>()
}
interface CallResult<T> {
fun enqueue(callback: (result: Result<T>) -> Unit)
fun clone(): CallResult<T>
fun isExecuted(): Boolean
fun execute(): Response<T>
}
/**
* Maps a [Call] to [CallResultMapper].
* */
internal class CallResultMapper<T>(private val call: Call<T>, private val callbackExecutor: Executor?, private val responseErrorMapper: ((Response<*>) -> Error?)? = null) : CallResult<T> {
override fun execute(): Response<T> {
return call.execute()
}
override fun enqueue(callback: (result: Result<T>) -> Unit) {
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (call.isCanceled) return
val result: Result<T> = if (response.isSuccessful) {
Result.Success(response.body())
} else {
Result.Failure(responseErrorMapper?.invoke(response) ?: RemoteServiceHttpError(response))
}
val runnable = {
callback(result)
}
callbackExecutor?.execute(runnable) ?: runnable()
}
override fun onFailure(call: Call<T>, t: Throwable) {
val error: Error = if (t is IOException) {
NetworkError()
} else {
UnexpectedError()
}
val runnable = {
callback(Failure(error))
}
callbackExecutor?.execute(runnable) ?: runnable()
}
})
}
override fun isExecuted(): Boolean {
return call.isExecuted
}
override fun clone(): CallResult<T> {
return CallResultMapper(call.clone(), callbackExecutor)
}
}
class UnexpectedError : Error() {
override fun toString(): String {
return "UnexpectedError"
}
}
/**
* Represents an error in which the server could not be reached.
*/
class NetworkError : Error() {
override fun toString(): String {
return "NetworkError"
}
}
/**
* An error response from the server.
*/
open class RemoteServiceError : Error() {
override fun toString(): String {
return "RemoteServiceError(message: $message)"
}
}
/**
* A Remote Service Error with a HttpStatus code.
*/
open class RemoteServiceHttpError(val response: Response<*>) : RemoteServiceError() {
val httpStatusCode = HttpStatusCode.values().firstOrNull { statusCode -> statusCode.code == response.code() } ?: Unknown
val isClientError: Boolean
get() = response.code() in 400..499
val isServerError: Boolean
get() = response.code() in 500..599
override fun toString(): String {
return "RemoteServiceHttpError" +
"\ncode: ${response.code()} (${httpStatusCode.name})" +
"\nmessage: ${super.message}"
}
}
enum class HttpStatusCode(val code: Int) {
Unknown(-1),
// Client Errors
BadRequest(400),
Unauthorized(401),
PaymentRequired(402),
Forbidden(403),
NotFound(404),
MethodNotAllowed(405),
NotAcceptable(406),
ProxyAuthenticationRequired(407),
RequestTimeout(408),
Conflict(409),
Gone(410),
LengthRequired(411),
PreconditionFailed(412),
PayloadTooLarge(413),
UriTooLong(414),
UnsupportedMediaType(415),
RangeNotSatisfiable(416),
ExpectationFailed(417),
ImATeapot(418),
MisdirectedRequest(421),
UnprocessableEntity(422),
Locked(423),
FailedDependency(424),
UpgradeRequired(426),
PreconditionRequired(428),
TooManyRequests(429),
RequestHeaderFieldsTooLarge(431),
UnavailableForLegalReasons(451),
// Server Errors
InternalServerError(500),
NotImplemented(501),
BadGateway(502),
ServiceUnavailable(503),
GatewayTimeout(504),
HttpVersionNotSupported(505),
VariantAlsoNegates(506),
InsufficientStorage(507),
LoopDetected(508),
NotExtended(510),
NetworkAuthenticationRequired(511);
}
@timusus
Copy link
Author

timusus commented May 20, 2021

I'm using a kind of variation of this in a couple of apps, including one with Coroutines. It's late here, but I'll see if I can provide an example for you tomorrow

@esQmo
Copy link

esQmo commented May 30, 2021

Waited for you, you've surely forgot. So here's a little reminder :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment