Skip to content

Instantly share code, notes, and snippets.

@yitz-grocerkey
Created June 13, 2017 19:32
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save yitz-grocerkey/61a66a15a0c22e8ea5149484676618c9 to your computer and use it in GitHub Desktop.
Save yitz-grocerkey/61a66a15a0c22e8ea5149484676618c9 to your computer and use it in GitHub Desktop.
Retrofit RxJava global error handling in Kotlin
// for any errors that should be handled before being handed off to RxJava.
// In other words global error logic.
// An example might be 401 when not logging in
import okhttp3.Interceptor
import okhttp3.Response
class ErrorInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain?): Response {
val originalResponse = chain!!.proceed(chain.request())
if (shouldLogout(originalResponse)) {
// your logout logic here
// send empty response down the chain
return Response.Builder().build()
}
return originalResponse
}
private fun shouldLogout(response: Response) : Boolean {
if (response.isSuccessful) {
return false
}
// 401 and auth token means that we need to logout
return (response.code() == 401 &&
!response.headers().names().contains(AUTH_HEADER_KEY))
}
}
import okhttp3.ResponseBody
import your.package.here.ServerError
import retrofit2.Converter
import retrofit2.Response
import retrofit2.Retrofit
import timber.log.Timber
import java.io.IOException
class RetrofitException(private val _message: String?,
private val _url: String?,
private val _response: Response<*>?,
private val _kind: Kind,
private val _exception: Throwable?,
private val _retrofit: Retrofit?
) : RuntimeException(_message, _exception) {
private var _errorData : ServerError? = null
companion object {
fun httpError(url: String, response: Response<*>, retrofit: Retrofit): RetrofitException {
val message = response.code().toString() + " " + response.message()
return RetrofitException(message, url, response, Kind.HTTP, null, retrofit)
}
fun httpErrorWithObject(url: String, response: Response<*>, retrofit: Retrofit): RetrofitException {
val message = response.code().toString() + " " + response.message()
val error = RetrofitException(message, url, response, Kind.HTTP_422_WITH_DATA, null, retrofit)
error.deserializeServerError()
return error
}
fun networkError(exception: IOException): RetrofitException {
return RetrofitException(exception.message, null, null, Kind.NETWORK, exception, null)
}
fun unexpectedError(exception: Throwable): RetrofitException {
return RetrofitException(exception.message, null, null, Kind.UNEXPECTED, exception, null)
}
}
/** The request URL which produced the error. */
fun getUrl() = _url
/** Response object containing status code, headers, body, etc. */
fun getResponse() = _response
/** The event kind which triggered this error. */
fun getKind() = _kind
/** The Retrofit this request was executed on */
fun getRetrofit() = _retrofit
/** The data returned from the server in the response body*/
fun getErrorData() : ServerError? = _errorData
private fun deserializeServerError() {
if (_response != null && _response.errorBody() != null) {
try {
_errorData = getErrorBodyAs(ServerError::class.java)
} catch (e: IOException) {
Timber.tag("Retrofit servererror deserialization").e(e)
}
}
}
/**
* HTTP response body converted to specified `type`. `null` if there is no
* response.
* @throws IOException if unable to convert the body to the specified `type`.
*/
@Throws(IOException::class)
fun <T> getErrorBodyAs(type: Class<T>): T? {
if (_response == null || _response.errorBody() == null || _retrofit == null) {
return null
}
val converter : Converter<ResponseBody, T> =
_retrofit.responseBodyConverter(type, arrayOfNulls<Annotation>(0))
return converter.convert(_response.errorBody())
}
enum class Kind {
/** An [IOException] occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
HTTP_422_WITH_DATA,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
}
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.io.IOException
import java.lang.reflect.Type
class WebRequest {
val retrofit: Retrofit
init {
val okBuilder = OkHttpClient().newBuilder()
okBuilder.networkInterceptors().add(ErrorInterceptor())
retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxErrorHandlingCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.client(okBuilder.build())
.build()
}
}
// Wraps "regular" Retrofit errors in custom RetrofitException class
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.io.IOException
import java.lang.reflect.Type
class RxErrorHandlingCallAdapterFactory: CallAdapter.Factory() {
private val _original by lazy {
RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())
}
companion object {
fun create() : CallAdapter.Factory = RxErrorHandlingCallAdapterFactory()
}
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *> {
val wrapped = _original.get(returnType, annotations, retrofit) as CallAdapter<out Any, *>
return RxCallAdapterWrapper(retrofit, wrapped)
}
private class RxCallAdapterWrapper<R>(val _retrofit: Retrofit,
val _wrappedCallAdapter: CallAdapter<R, *>
): CallAdapter<R, Observable<R>> {
override fun responseType(): Type = _wrappedCallAdapter.responseType()
@Suppress("UNCHECKED_CAST")
override fun adapt(call: Call<R>): Observable<R> {
val adapted = (_wrappedCallAdapter.adapt(call) as Observable<R>)
adapted.onErrorResumeNext { throwable: Throwable ->
Observable.error(asRetrofitException(throwable))
}
return adapted
}
private fun asRetrofitException(throwable: Throwable): RetrofitException {
// We had non-200 http error
if (throwable is HttpException) {
val response = throwable.response()
if (throwable.code() == 422) {
// on out api 422's get metadata in the response. Adjust logic here based on your needs
return RetrofitException.httpErrorWithObject(response.raw().request().url().toString(), response, _retrofit)
} else {
return RetrofitException.httpError(response.raw().request().url().toString(), response, _retrofit)
}
}
// A network error happened
if (throwable is IOException) {
return RetrofitException.networkError(throwable)
}
// We don't know what happened. We need to simply convert to an unknown error
return RetrofitException.unexpectedError(throwable)
}
}
}
.subscribe({
// do success action
}, { it ->
view.displayError(getLoginErrorMessage(it))
})
private fun getLoginErrorMessage(exception: Throwable) : String {
if (exception is RetrofitException) {
when (exception.getKind()) {
RetrofitException.Kind.HTTP_422_WITH_DATA ->
return exception.getErrorData()!!.getMessage()
RetrofitException.Kind.HTTP ->
return R.string.default_http_error_message
RetrofitException.Kind.NETWORK ->
return R.string.default_network_error_message
RetrofitException.Kind.UNEXPECTED->
return R.string.default_unexpected_error_message
}
}
return R.string.default_error_message
}
@abbath0767
Copy link

here you have a error in error handling -

override fun adapt(call: Call<R>): Observable<R> {
            val adapted = (_wrappedCallAdapter.adapt(call) as Observable<R>)
            adapted.onErrorResumeNext { throwable: Throwable ->     //need return here
                Observable.error(asRetrofitException(throwable))
            }

            return adapted .   //and not here
        }

@lby1992
Copy link

lby1992 commented Jun 14, 2018

@abbath0767

adapted.onErrorResumeNext { throwable: Throwable ->     //need return here
                Observable.error(asRetrofitException(throwable))
            }

The return here can be omitted.

return adapted .   //and not here

This return statement is necessary of the adapt method.

@ivnsch
Copy link

ivnsch commented Jan 7, 2020

Crashing here:

val adapted = (_wrappedCallAdapter.adapt(call) as Observable<R>)
Caused by: java.lang.ClassCastException: io.reactivex.internal.operators.observable.ObservableSingleSingle cannot be cast to io.reactivex.Observable
at ...$RxCallAdapterWrapper.adapt(RxErrorHandlingCallAdapterFactory.kt:37)
at ...$RxCallAdapterWrapper.adapt(RxErrorHandlingCallAdapterFactory.kt:28)

retrofit 2.7.1

Edit: The reason was that my call was returning Single instead of Observable.

@bayoudhdev
Copy link

To fix this Crash :
Caused by: java.lang.ClassCastException: io.reactivex.internal.operators.observable.ObservableSingleSingle cannot be cast to io.reactivex.Observable
at ...$RxCallAdapterWrapper.adapt(RxErrorHandlingCallAdapterFactory.kt:37)
at ...$RxCallAdapterWrapper.adapt(RxErrorHandlingCallAdapterFactory.kt:28)

Solution :
override fun adapt(call: Call): Any {
return when (val result = _wrappedCallAdapter.adapt(call)) {
is Single<> -> result.onErrorResumeNext(Function { throwable -> Single.error(asOneAppServiceException(throwable)) })
is Observable<
> -> result.onErrorResumeNext(Function { throwable -> Observable.error(asOneAppServiceException(throwable)) })
is Completable -> result.onErrorResumeNext(Function { throwable -> Completable.error(asOneAppServiceException(throwable)) })
is Flowable<*> -> result.onErrorResumeNext(Function { throwable -> Flowable.error(asOneAppServiceException(throwable)) })
else -> result
}
}

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