5
votes

I'm trying to implement a refresh token strategy in Swift 5 and the Combine Framework for iOS.

I don't plan on using any third party package, just using what is provided by the framework, `URLSession.dataTaskPublisher`, so mu goal is to :

  1. Make a request
  2. If the request fails with 401, refresh the auth token (which is another request)
    1. After the refresh token is done, retry the first request
    2. If it fails throw the error to be handled by the caller

This is a very trivial use case, but seems to be very hard to implement in Combine, that makes it really hard to use in any real life scenario.

Any help would be welcome !

This is my try, which unfortonately doesn't work

private func dataTaskPublisherWithAuth(for request: URLRequest) -> URLSession.DataTaskPublisher {
    return session.dataTaskPublisher(for: request)

        .tryCatch { error -> URLSession.DataTaskPublisher in
guard error.errorCode == 401 else {
throw error
}
var components = URLComponents(url: self.baseUrl, resolvingAgainstBaseURL: true)
components?.path += "/refresh"
components?.queryItems = [
URLQueryItem(name: "refresh_token", value: KeychainHelper.RefreshToken),
]

let url = components?.url
var loginRequest = URLRequest(url: url!)
loginRequest.httpMethod = "GET"
loginRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

return session.dataTaskPublisher(for: loginRequest)
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.map { result in
if result.accessToken != nil {
// Save access token
KeychainHelper.AccessToken = result.accessToken!
KeychainHelper.RefreshToken = result.refreshToken!
KeychainHelper.ExpDate = Date(timeIntervalSinceNow: TimeInterval(result.expiresIn!))
}
return result
}
.flatMap { data -> URLSession.DataTaskPublisher in
session.dataTaskPublisher(for: request)
}
    }.eraseToAnyPublsiher()

}
1
I am trying to implement exactly the same use case. Can I ask did you achieve this successfully? - RobbeR

1 Answers

6
votes

You should use the .tryCatch method on Publisher here. This lets you replace an error with another publisher (such as replacing error 401 with a refresh request followed by a map switchToLastest auth request) or with another error (in this case if its not a 401 then just throw the original error).

Note that you probably shouldn't be using flatMap here because its not the same as .flatMapLatest in Rx or .flatmap(.latest) in Reactive Swift. You want to get into the habit of using .map and switchToLatest in Combine (ie apple decided the flattening and the mapping are two separate operators). If you don't do this you will get into trouble in some places that generate more than one inner publisher, such as search while you type, because instead of getting the latests inner value you will get ALL of them, in arbitrary order since the network requests complete in indeterminate time.