Reputation: 370
I have an API Service handler implemented which uses an authentication token in the header of the request. This token is fetched when the user logs in at the launch of the application. After 30 minutes, the token is expired. Thus, when a request is made after this timespan, the API returns an 403 statuscode. The API should then login again and restart the current API request.
The problem I am encountering is that the login function to fetch a new token, makes use of a completion handler to let the calling code know if the asynchronous login procedure has been successful or not. When the API gets a 403 statuscode, it calls the login procedure and and when that is complete, it should make the current request again. But this repeated API request should return some value again. However, returning a value is not possible in a completion block. Does anyone know a solution for the problem as a whole?
The login function is as follows:
func login (completion: @escaping (Bool) -> Void) {
self.loginState = .loading
let preparedBody = APIPrepper.prepBody(parametersDict: ["username": self.credentials.username, "password": self.credentials.password])
let cancellable = service.request(ofType: UserLogin.self, from: .login, body: preparedBody).sink { res in
switch res {
case .finished:
if self.loginResult.token != nil {
self.loginState = .success
self.token.token = self.loginResult.token!
_ = KeychainStorage.saveCredentials(self.credentials)
_ = KeychainStorage.saveAPIToken(self.token)
completion(true)
}
else {
(self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = ("ERROR", "TOKEN", "error", true)
self.loginState = .failed(stateIdentifier: "TOKEN", errorMessage: "ERROR")
completion(false)
}
case .failure(let error):
(self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = (error.errorMessage, error.statusCode, "error", true)
self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
completion(false)
}
} receiveValue: { response in
self.loginResult = response
}
self.cancellables.insert(cancellable)
}
The API service is as follows:
func request<T: Decodable>(ofType type: T.Type, from endpoint: APIRequest, body: String) -> AnyPublisher<T, Error> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue(KeychainStorage.getAPIToken()?.token, forHTTPHeaderField: "token")
}
if !body.isEmpty {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in Error.unknown}
.flatMap { data, response -> AnyPublisher<T, Error> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: Error.unknown).eraseToAnyPublisher()
}
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
return Just(data)
.decode(type: T.self, decoder: jsonDecoder)
.mapError { _ in Error.decodingError }
.eraseToAnyPublisher()
}
else if response.statusCode == 403 {
let credentials = KeychainStorage.getCredentials()
let signinModel: SigninViewModel = SigninViewModel()
signinModel.credentials = credentials!
signinModel.login() { success in
if success == true {
-------------------> // MAKE THE API CALL AGAIN AND THUS RETURN SOME VALUE
}
else {
-------------------> // RETURN AN ERROR
}
}
}
else if response.statusCode == 429 {
return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: "Oeps! Je hebt teveel verzoeken gedaan, wacht een minuutje")).eraseToAnyPublisher()
}
else {
do {
let errorMessage = try jsonDecoder.decode(APIErrorMessage.self, from: data)
return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: errorMessage.error ?? "Er is iets foutgegaan")).eraseToAnyPublisher()
}
catch {
return Fail(error: Error.decodingError).eraseToAnyPublisher()
}
}
}
.eraseToAnyPublisher()
}
Upvotes: 1
Views: 1909
Reputation: 87605
You're trying to combine Combine with old asynchronous code. You can do it with Future
, check out more about it in this apple article:
Future { promise in
signinModel.login { success in
if success == true {
promise(Result.success(()))
}
else {
promise(Result.failure(Error.unknown))
}
}
}
.flatMap { _ in
// repeat request if login succeed
request(ofType: type, from: endpoint, body: body)
}.eraseToAnyPublisher()
But this should be done when you cannot modify the asynchronous method or most of your codebase uses it.
In your case it looks like you can rewrite login
to Combine. I can't build your code, so there might be errors in mine too, but you should get the idea:
func login() -> AnyPublisher<Void, Error> {
self.loginState = .loading
let preparedBody = APIPrepper.prepBody(parametersDict: ["username": self.credentials.username, "password": self.credentials.password])
return service.request(ofType: UserLogin.self, from: .login, body: preparedBody)
.handleEvents(receiveCompletion: { res in
if case let .failure(error) = res {
(self.banner.message,
self.banner.stateIdentifier,
self.banner.type,
self.banner.show) = (error.errorMessage, error.statusCode, "error", true)
self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
}
})
.flatMap { loginResult in
if loginResult.token != nil {
self.loginState = .success
self.token.token = loginResult.token!
_ = KeychainStorage.saveCredentials(self.credentials)
_ = KeychainStorage.saveAPIToken(self.token)
return Just(Void()).eraseToAnyPublisher()
} else {
(self.banner.message, self.banner.stateIdentifier, self.banner.type, self.banner.show) = ("ERROR",
"TOKEN",
"error",
true)
self.loginState = .failed(stateIdentifier: "TOKEN", errorMessage: "ERROR")
return Fail(error: Error.unknown).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
And then call it like this:
signinModel.login()
.flatMap { _ in
request(ofType: type, from: endpoint, body: body)
}.eraseToAnyPublisher()
Upvotes: 3