I found this example on How to refresh oauth token using moya and rxswift which I had to alter slightly to get to compile. This code works 80% for my scenario. The problem with it is that it will run for all http errors, and not just 401 errors. What I want is to have all my other http errors passed on as errors, so that I can handle them else where and not swallow them here.
With this code, if I get a HttpStatus 500
, it will run the authentication code 3 times which is obviously not what I want.
Ive tried to alter this code to handle only handle 401
errors, but it seem that no matter what I do I can't get the code to compile. It's always complaining about wrong return type, "Cannot convert return expression of type Observable<Response> to return type Observable<Response>"
which makes no sense to me..
What I want: handle 401, but stop on all other errors
import RxSwift
import KeychainAccess
import Moya
public extension ObservableType where E == Response {
/// Tries to refresh auth token on 401 errors and retry the request.
/// If the refresh fails, the signal errors.
public func retryWithAuthIfNeeded() -> Observable<E> {
return self.retryWhen {
(e: Observable<ErrorType>) in
return Observable.zip(e, Observable.range(start: 1, count: 3), resultSelector: { $1 })
.flatMap { i in
return AuthProvider.sharedInstance.request(
.LoginFacebookUser(
accessToken: AuthenticationManager.defaultInstance().getLoginTokenFromKeyChain(),
useFaceBookLogin: AuthenticationManager.defaultInstance().isFacebookLogin())
)
.filterSuccessfulStatusCodes()
.mapObject(Accesstoken.self)
.catchError {
error in
log.debug("ReAuth error: \(error)")
if case Error.StatusCode(let response) = error {
if response.statusCode == 401 {
// Force logout after failed attempt
log.debug("401:, force user logout")
NSNotificationCenter.defaultCenter().postNotificationName(Constants.Notifications.userNotAuthenticated, object: nil, userInfo: nil)
}
}
return Observable.error(error)
}.flatMapLatest({
token -> Observable<Accesstoken> in
AuthenticationManager.defaultInstance().storeServiceTokenInKeychain(token)
return Observable.just(token)
})
}
}
}
}
Which line has the compilation error? It seems to me that it would be this line:
.catchError {
error in
//...
return Observable.error(error) // is this the line causing the compilation error?
}
If so, it's probably because catchError
is expecting the block to return an Observable<Response>
with which it can continue in case of an error, and not an Observable<ErrorType>
.
In either case, it helps to annotate your code with more types so that you can pinpoint problems like this, as well as help the Swift compiler, which often can't figure out these kinds of things on its own. So something like this would have helped you:
.catchError {
error -> Observable<Response> in
//...
return Observable.error(error) // Swift should have a more accurate and helpful error message here now
}
Note that I'm only showing you what the error is and how to get Xcode to give you better error messages. What you're trying to return still isn't correct.
401
I'm not sure why you're expecting this code to treat 401
differently (other than posting to the notification center and logging). As it is, you're catching the error, but you're always returning an Observable
with an Error
event at the end (return Observable.error(error)
), so it will never retry.
To get 401
to retry, you should return an Observable
from the retryWhen
block, which will send a Next
event (signifying that you want to retry). For all other status codes, that Observable
should send an Error
(as you're currently doing), which will signify that you don't want to retry, and that you'd like the error propagated.
So something like this:
.retryWhen { errorObservable -> Observable<ErrorType> in
log.debug("ReAuth error: \(error)")
if case Error.StatusCode(let response) = error where response.statusCode == 401 {
log.debug("401:, force user logout")
NSNotificationCenter.defaultCenter().postNotificationName(Constants.Notifications.userNotAuthenticated, object: nil, userInfo: nil)
// If `401`, then return the `Observable<ErrorType>` which was given to us
// It will emit a `.Next<ErrorType>`
// Since it is a `.Next` event, `retryWhen` will retry.
return errorObservable
}
else {
// If not `401`, then `flatMap` the `Observable<ErrorType>` which
// is about to emit a `.Next<ErrorType>` into
// an `Observable<ErrorType>` which will instead emit a `.Error<ErrorType>`.
// Since it is an `.Error` event, `retryWhen` will *not* retry.
// Instead, it will propagate the error.
return errorObservable.flatMap { Observable.error($0) }
}
}