Never see a 401 error again.

RxSwift and Handling Invalid Tokens

  1. Since URLRequests will potentially need to be created with new tokens, I need a function that can create a request when given a token.
  2. Since retryWhen provides an Observable<Error> that emits every error the stream throws, I need a function that will refresh the token and emit a trigger event on unauthorized errors while passing all other errors down the line.
  3. Since multiple requests could be unauthorized while the service is waiting for a new token, I need a way to notify all the requests once the token is provided.
/**
Builds and makes network requests using the token provided by the
service. Will request a new token and retry if the result is an
unauthorized (401) error.
- parameter response: A function that sends requests to the network
and emits responses. Can be for example
`URLSession.shared.rx.response`
- parameter tokenAcquisitionService: The object responsible for
tracking the auth token. All requests should use the same
object.
- parameter request: A function that can build the request when
given a token.
- returns: response of a guaranteed authorized network request.
**/
typealias Response = (URLRequst) -> Observable<(response: HTTPURLResponse, data: Data)>
typealias Request = (String) -> URLRequest
func getData(response: @escaping Response, tokenAcquisitionService: TokenAcquisitionService, request: @escaping Request) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable
.deferred { tokenAcquisitionService.token.take(1) }
.map { request($0) }
.flatMap { response($0) }
.map { response in
guard response.response.statusCode != 401 else { throw ResponseError.unauthorized }
return response
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
}
  • .deferred { tokenAcquisitionService.token.take(1) } will subscribe to, and pass on, the token emitted by the service whenever this observable is subscribed to. In effect, when the request is started and whenever it is retried, this line will get the latest token from the service.
  • .map { request($0) } will build the request with the token provided.
  • .flatMap { response($0) } will send the network request and wait for the response.
  • guard response.response.statusCode != 401 else { throw ResponseError.unauthorized } will cause the observable to emit an error if the response was rejected for being unauthorized.
  • .retryWhen { $0.renewToken(with: tokenAcquisitionService) } will give the service a chance to handle the .unauthorized error, but pass on any other error.

func trackErrors(for:)

  • guard (error as? ResponseError) == .unauthorized else { throw error} will throw any error other than the one it is watching for. The idea is that if something else goes wrong, the calling code would want to know. An .unauthorized error will cause a next event to emit.
  • do(onNext:) The block handed to this operator will send a next event to the private Subject that handles the chain which gets a new token. This is run through a subject because all the network requests that need to retry must be merged together so they can all be notified once the new token is acquired. The recursive lock guards the subject against events from different threads.
  • filter { _ in false } eats all the next events emitted by the chain. This is because we don’t want to trigger a retry until after the new token has been acquired, but we still want to pass on any stop events (completed or an error other than an unauthorized network request.)
  • Observable.merge(token.skip(1).map { _ in }, error) Remember that subscriptions to token will immediately receive a next event but this is unnecessary because we already know that the current token is invalid. We want the trigger for the next token. We also want all the stop events from the error observable (remember, this observable won’t emit next events.

init(initialToken:getToken:extractToken:)

  • relay.flatMapFirst { getToken() } is the line that requests a new token and makes sure that only one request is made for each batch of unauthorized requests that are waiting.
  • guard urlResponse.response.statusCode / 100 == 2 else { throw ResponseError.refusedToken(response: urlResponse.response, data: urlResponse.data) } will inform all listening requests that the service was unable to successfully extract a token, otherwise it will extract the token and emit it.
  • startWith(initialToken) ensures that the token property is pre-loaded with a value.
  • share(replay: 1) ensures that any observers that subscribe to the token property will immediately get the most recently acquired token value so they can attempt the first request.

FAQ:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store