Fairly new to Combine. A common scenario using access tokens and refresh token.
You get a 401 and you need to handle it (call some service to refresh the token) before retrying the initial call again
func dataLoader(backendURL: URL) -> AnyPublisher<Data, Error> {
let request = URLRequest(url: backendURL)
return dataPublisher(for: request)
// We get here when a request fails
.tryCatch { (error) -> AnyPublisher<(data: Data, response: URLResponse), URLError> in
guard error.errorCode == 401 else { // UPS - Unauthorized request
throw error
}
// We need to refresh token and retry -> HOW?
// And try again
// return dataPublisher(for: request)
}
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw CustomError.invalidServerResponse
}
return data
}
.eraseToAnyPublisher()
}
How would I go about wrapping this "token refresh service"?
Your code mentions a “token” but you don't explain what that is. Let's assume you have a token type:
struct Token: RawRepresentable {
var rawValue: String
}
And let's say you have a function that gets a fresh token asynchronously, by returning a publisher of the fresh token:
func freshToken() -> AnyPublisher<Token, Error> {
// Your code here, probably involving a URL request/response...
fatalError()
}
And let's say you generate the URL request for the data by combining some URL with the token:
func backendRequest(with url: URL, token: Token) -> URLRequest {
// Your code here, to somehow combine the url and the token into the real ...
fatalError()
}
Now you want to retry the request, with a fresh token each time, if the response is a 404. You should probably limit the number of tries. So let's write the function to take a triesLeft
count. If triesLeft > 1
and the response is 404, it will ask for a fresh token and use that to call itself again (with triesLeft
decremented).
The goal is made more complex because URLSession.DataTaskPublisher
doesn't turn a 404 response into an error. It treats it as a normal output.
So we'll use a nested helper function to process the output of the DataTaskPublisher
, so that we don't have so much code nested inside closures. The helper function, named publisher(forDataTaskOutput:)
, decides what to do based on the response.
If the response is an HTTP response with code 200, it just returns the data. Note that it has to return a publisher whose Failure
is Error
, so it uses a Result.Pubilsher
and lets Swift deduce the Failure
type.
If the response is an HTTP response with code 404, and triesLeft > 1
, it calls freshToken
and uses flatMap
to chain that into another call to the outer function.
Otherwise, it produces a failure with error CustomError.invalidServerResponse
.
func data(atBackendURL url: URL, token: Token, triesLeft: Int) -> AnyPublisher<Data, Error> {
func publisher(forDataTaskOutput output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<Data, Error> {
switch (output.response as? HTTPURLResponse)?.statusCode {
case .some(200):
return Result.success(output.data).publisher.eraseToAnyPublisher()
case .some(404) where triesLeft > 1:
return freshToken()
.flatMap { data(atBackendURL: url, token: $0, triesLeft: triesLeft - 1) }
.eraseToAnyPublisher()
default:
return Fail(error: CustomError.invalidServerResponse).eraseToAnyPublisher()
}
}
let request = backendRequest(with: url, token: token)
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { $0 as Error }
.flatMap(publisher(forDataTaskOutput:))
.eraseToAnyPublisher()
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With