Reputation: 2483
I am trying to work around 401 error scenario where I want to catch the error, check if error is 401 and if it is,
Currently, I am doing something like below:
return urlSession.dataTaskPublisher(for: request)
.tryMap(checkForAPIError)
.tryCatch { (error) -> AnyPublisher<(data: serverData, response: URLResponse), URLError> in
self.fetchoAuthToken()
.tryMap { (token) in
// Saves token
}
.receive(on: RunLoop.main)
.subscribe(on: DispatchQueue.main)
.sink { (completion) in
// Completion handling here
} receiveValue: { (value) in
print("Received \(value)")
}
.store(in: &self.subscription)
return self.urlSession.dataTaskPublisher(for: request)
}
.tryMap(parseJson)
.retry(3)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
The current problem for me is, while API self.fetchoAuthToken()
is still in execution, block returns new request. Which then executes with old tokens.
I want for self.fetchoAuthToken()
to execute synchronously so return can be done after it executes and new tokens can be used.
Any help would be appreciated.
Upvotes: 1
Views: 1834
Reputation: 32853
As others have said, the side effects (e.g. saving the token) should not be part of the processing pipeline (or at least not directly).
Recommending to split the request logic in two, by adding a few helper functionalities:
/// this will do the actual URLSession call
/// fetchoAuthToken will likely call this with `withToken: false)
func _sendRequest(_ request: URLRequest, withToken: Bool = true) -> AnyPublisher<(data: Data, response: URLResponse), Error> {
let request = withToken ? addToken(to: request) : request
return urlSession
.dataTaskPublisher(for: request)
.mapError { $0 }
.eraseToAnyPublisher()
}
/// name explicit enough :)
func addToken(to request: URLRequest) -> URLRequest {
// do whathever needed (headers, cookies, etc)
}
enum MyError: Error {
/// `fetchoAuthToken` and `checkForAPIError` should return this
/// in case the request fails with a 401/403, or maybe some other
/// conditions
case notAuthenticated
}
Now, if you encapsulate the token fetching and saving within fetchoAuthToken
, and implement checkForAPIError
to return MyError.notAuthenticated
in case the request failed due to that reason, then you can end up with a nice "pure" pipeline:
return _sendRequest(request)
.tryMap(checkForAPIError)
.tryCatch { error -> AnyPublisher<(data: Data, response: URLResponse), Error> in
if error as? MyError == .notAuthenticated {
return fetchToken().flatMap { _sendRequest(request) }.eraseToAnyPublisher()
} else {
throw error
}
}
.tryMap(parseJson)
.retry(3, unless: \.isNotAuthenticated)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
Another problem that you might want to address is the retry
part - if fetchoAuthToken
fails due to invalid credentials, then you'll likely want to skip the retry part, as if the credentials are invalid the first time, most likely they also will be invalid the second and third time.
For this problem, you can use this answer, and do a retry unless
:
extension Error {
/// enabling sugar syntax for `retry`
var isNotAuthenticated: Bool { self as? MyError == .notAuthenticated }
}
return _sendRequest(request)
// rest of the pipeline omitted for readability
.retry(3, unless: \.isNotAuthenticated)
Upvotes: 0
Reputation: 49590
You need to chain the publishers, and return the chain as a new publisher from tryCatch
.
You should typically avoid side-effects, but if you must - like saving the OAuth token, do that in .handleEvents
, instead of creating a sink
subscription.
return urlSession.dataTaskPublisher(for: request)
.tryMap(checkForAPIError)
.tryCatch { error in
self.fetchoAuthToken()
.handleEvents(receiveOutput: { (token) in
// Saves token
})
.flatMap { _ in
urlSession.dataTaskPublisher(for: request)
}
}
.tryMap(parseJson)
.retry(3)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
Upvotes: 1