Swift semaphore behavior - swift

I've a problem. I need that a resource it's ready to use it. I've implemented this mechanism:
func shouldRetry(request: Request, error: Error) -> Bool {
let semaphore = DispatchSemaphore(value: 0)
var shouldRetry: Bool = false
self.interceptor?.retry(request.response, for: self.session!, dueTo: error, completion: { (retryResult) in
switch retryResult {
case .retry:
shouldRetry = true
semaphore.signal()
case .doNotRetry:
shouldRetry = false
semaphore.signal()
case .retryWithDelay(let delay):
shouldRetry = true
self.retryTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { (_) in
semaphore.signal()
self.retryTimer?.invalidate()
self.retryTimer = nil
})
}
})
semaphore.wait()
return shouldRetry
}
The problem is that interceptor?.retry func use is called in the same thread of semaphore, and this block the process. Any suggestions?
UPDATE:
I solved my question. I subclasses URLProtocol abstract class. This class works with URLSession and it's possible suspend a HTTP call, make async code and resume

I don't think there's an answer without more context. However, if you try to implement a "refresh-token adapter / retrier", the strategy is as follows:
You need a specialised adapter (a struct or class) which sets the Authorization header with the access token. Accessing a token MUST be thread-safe, and even getting a token may fail - for example, because there's none. In case of an error, the adapter tries NOT to get a new access token, this will be done in the retrier. The adapter may just not set the access token. The request will fail and will be handled in the retrier.
The adapter dispatches is functions on a dedicated queue, let's call it "process_queue". When finished setting the header it calls the completion handler.
You also need a specialised Retrier (a struct or class). It accesses a shared state which holds the result of a token request. This can be a Swift.Result for example.
This retrier's functions execute on a dedicated dispatch queue.
The specialised Retrier determines the response status and if the status code is a 401 (not authorised) AND if the Authorization header is a bearer token, it needs to make a refresh-token request.
When starting this refresh-token task, it suspenses the process_queue, so that no more request adaption take place with an expired access token.
Now, the token request completes and say it was successful. The retrier stores the access token (into the key chain), updates the original request with the new access token, then resumes the process_queue and then it calls its completion handler.
Proper Error handling makes this slightly more complex. The question is how to treat pending requests when the refresh-token request has failed. I have used an approach, which let fail all the pending requests that have been queued until the refresh-token request completed. They just fail with the error 401. Then a new cycle starts, beginning with attempting to get a new refresh-token.
This approach prevents that multiple failed requests call into the token endpoint at the same time. You can even extend the approach when the refresh token also expires. In that case you need to sign-in the user. All this may happen within the retrier. It completes only after sign-in is complete, after getting a new access token is complete, or when an error occurred.

Related

Got timeout error -1001 after some HTTP requests using Swift

I create an http request using PUT to get data from the server. I got this data from the server and transform it on a PDF file.
When I run it for the first time everything runs fine, but after some calls, I start to get timeout errors.
Sometimes, I need to restart the app to be able to receive HTTP requests again.
This is the code that I use.
func callGetPdfFromEndpointUsingNSMutableURLRequest() {
if codigoBarra == "" {
messageError = "Código não localizado"
showingAlert = true
redirectToPdfView = false
showingActivityIndicator = false
return
}
let serviceRepository = ServiceRepository()
// let codigo_barra = "d152d36914313fedfbf36842a7195b723"
let json: [String: Any] = ["codigoBarra":"\(codigoBarra)"]
let request: NSMutableURLRequest = serviceRepository.clientURLRequest(endpointPesquisa, typeAutho: .basic, parms: "", body: json as Dictionary<String, AnyObject>)
print("request: \(request)")
print("request.url: \(String(describing: request.url))")
serviceRepository.put(request, retryLogin: true, completion: {isOk,msgError,httpCode,needLogin, response in
if isOk {
tratarRequisicaoPdf(response)
} else {
print("erro no request - is not ok | - httpCode: \(httpCode)")
var stringResponse:String = ""
if response != nil {
stringResponse = String(data: response as! Data, encoding: .utf8)!
} else {
stringResponse = "Sem resposta do servidor, tempo limite da solicitação foi esgotado."
}
messageError = "\(stringResponse)"
print(messageError)
showingAlert = true
redirectToPdfView = false
semaphore.signal()
}
semaphore.wait()
showingActivityIndicator = false
})
}
This error is unstable, sometimes it shows, sometimes it don't appear.
The people working on backend was not able to detect any problems.
I got the following error:
2022-05-20 15:33:15.442419-0300 CDA[2016:38068] Task <147B6F7F-E46A-47D0-A258-D6F3E5417D7E>.<1> finished with error [-1001] Error Domain=NSURLErrorDomain Code=-1001 "Esgotou-se o tempo limite da solicitação." UserInfo={_kCFStreamErrorCodeKey=-2102, NSUnderlyingError=0x7fd0e5245520 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <147B6F7F-E46A-47D0-A258-D6F3E5417D7E>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <147B6F7F-E46A-47D0-A258-D6F3E5417D7E>.<1>"
), NSLocalizedDescription=Esgotou-se o tempo limite da solicitação., NSErrorFailingURLStringKey=https://myurl.sp.gov.br/gedave/api/spservicos/v1/buscaRequisicaoExame, NSErrorFailingURLKey=https://myurl.sp.gov.br/gedave/api/spservicos/v1/buscaRequisicaoExame, _kCFStreamErrorDomainKey=4}
response::: nil
erro: response is nil
httpCode: 0
What can I do to try to detect what was causing the timeout errors?
Edit:
I added the source code that can be viewed here https://swiftfiddle.com/mz2dxw6z6bda7a6t44cryncrpi.
NEW EDIT:
Answers to comments created by #Rob
Good to know about the NSUrlMutableRequest
I will try to use the 'finishAndInvalidate' in my URLSession. I didn't know about that.
My problem is unpredictable. Now I start the app and the first call got an timeout, after the second call the app works. Sometimes it starts working, but after some requests, I got a timeout
In the absence of a MCVE, there is not enough here to diagnose or reproduce the problem.
That having been said, there are some interesting clues:
You are calling wait (on a semaphore). Eliminate the DispatchSemaphore and if you want to know when the requests are done, use DispatchGroup. But when you use the dispatch group, use notify when it is done, not wait. Avoid blocking threads unnecessarily. And never block the main thread.
Your network request is performed by ServiceRepository, which you have not shared with us. But in your fiddle link, you show us unrelated URLSession code that is specifying the main queue as its delegate queue. If ServiceRepository is doing something similar, that, combined with the wait, above, could easily deadlock your code.
So, eliminate the semaphore, avoid ever calling wait (whether semaphore or dispatch group), and the deadlock risk is eliminated.
That having been said, that is only one potential timeout risk. The other scenario might be that you are simply issuing too many requests for the URLSession to run them without timing out.
If that is the case, you have a few options:
Increase the timeout threshold (either of the request or the session).
Try bumping up the timeout values and see if that mitigates the issue.
Submit uploads in just-in-time manner.
E.g., you might have a process where, rather than initiating all the uploads at once, that you issue each upload upon the completion of the prior upload. Or you can wrap the upload in a custom, asynchronous, Operation subclass (which is complicated in its own right), and then tell the operation queue to only attempt 4 at a time, or whatever. There are many techniques to tackle this, but the idea is to prevent timeouts by not even submitting the upload until you are confident that the URLSession can perform them.
Use background URLSession with file-based uploads.
If you have uploads that are so slow that they risk timing out, it begs the question of whether you really want to force the user to keep the app running in order to let the uploads finish at all. If you use a background URLSession, the background daemon will not let them timeout, but rather will upload them when it can.
Refactoring this for background URLSession is a non-trivial exercise (the first time you do it, anyway). You have to abandon completion handlers, use delegate-methods API, use file-based uploads, etc. But it is an incredibly powerful way to let uploads proceed in the background, allowing the user to leave the app, etc.
I don't see full code but you should have a look into semaphore usage - probably wrong multithread logic leads to not complete you request and your completion is hovered for a long period and causes URLDataTask to produce the timeout error

Swift Alamofire RequestInterceptor not called if using SessionDelegate

I am trying to refresh my authentication credential for my request using Alamofire's RequestInterceptor.
I noticed I am unable to enter the validate() portion of the interceptor as my breakpoints show me I am entering my SessionDelegate handlers instead. More precisely the urlSession(_:dataTask:didReceive:completionHandler:) and urlSession(_:task:didCompleteWithError:) methods of my SessionDelegate class seems to swallow the logic for the RequestInterceptor. If I remove these two handlers from my SessionDelegate I am able to enter the validate() code which then triggers the refresh method on my Authenticator class.
Is there a way to force the RequestInteceptor to intercept my request and refresh my credential if needed before it enters my SessionDelegate handlers? Removing the handlers from my SessionDelegate is not an option as I need to perform specific business logic in there.
Code:
self.session = Session(delegate: mySessionDelegate, interceptor: myRequestInterceptor)
self.session.request(myURL)
.validate({ _, response, data -> DataRequest.ValidationResult in
let statusCode = response.statusCode
//this code is not entered if I have my SessionDelegate handler
if 200 ... 299 ~= statusCode { return .success(Void()) }
return .failure(AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: statusCode)))
})
.resume()
If you're writing a custom SessionDelegate subclass (which isn't recommended; there's almost always a better way to accomplish what you need), it's your responsibility to call super in your overridden methods, as they contain important events. Otherwise you'll break important Alamofire events, as you've found.

How to check for Auth user in Swift while allowing JWT Refresh?

I want to be able to have a single Bool method that checks if a user is logged in, and if so returns the token to be used in the API header.
func isLoggedIn() -> Bool {
if let authToken = AuthManager.loadAuthToken() {
if authToken.expiration.isGreaterThanDate(Date()) {
return true
} else {
let authAPI = AuthenticationAPI()
authAPI.refreshToken(token: authToken.token) { (results) in
switch results {
case .success(let response):
[HAVE PARENT METHOD RETURN TRUE]
case .failure:
removeAuthToken()
[HAVE PARENT METHOD RETURN FALSE]
}
}
}
}
return false
}
I have this mostly working, however, I am running into a wall trying to figure out how to allow the app to attempt the refresh token call when there is a token present, but it is expired.
Semaphores seem like the way to go, but seem to quickly complicate the code. Is this the way it should be done or is there a better approach that is usually done for this?
I take a different approach.
In an AuthManager type of class, I store the token in an optional var. If the token is present, then I assume it is valid to be used in an API call. In the didSet for the token, there is a Notification.Name.authStatusDidChange notification which is sent so the app can respond accordingly.
If the API call returns a 401 then I notify the AuthManager to clear the token (which triggers the notification, and in my case shows the login flow).
However, you could have additional logic which rather than clearing the token, the Authmanager first tries to refresh it. If the refresh succeeds, then a different notification is sent to retry the API request. Otherwise it clears the token and triggers a sign in.
While I was Typing - Like Michal said, with a little more detail.

cancel request when its already finished

consider the following (pseudo) code:
let request = Alamofire.request(...) {
//handler called when requests has been completed
//do some processing here
}
//some processing here, could take a while
request.cancel()
Question:
what happens if the request has already been fully completed (and the handler called) when the request.cancel() is done?
Will this return an error?
Is the handler called again?
Or (what I am hoping for) nothing...?
If the handler called that means request has its answer that can mean two things: Either request is succesfull, you have what you asked or request is not succesfull which means you will get an error.
Either way if you got your response request.cancel() will mean nothing.

Retry an NSURLSessionDataTask request after re-authorizing (AKA NSInvocation equivalent in Swift)

TL;DR version: What is the nearest equivalent to NSInvocation - or similar way to package up a method call, including arguments - in Swift?
Background: I have a ServerController object which is used across my app to make requests to an API. The magic happens in this method:
private func makeRequestWithPath(
path: String,
method: String = "GET",
body: NSData? = nil,
successBlock: (AnyObject?) -> Void) {
// ...
self.urlSession.dataTaskWithRequest(request,
completionHandler: { (data, resp, error) -> Void in
if let httpResp = resp as? NSHTTPURLResponse {
switch httpResp.statusCode {
case 200:
successBlock(/* pass process response in here */)
case 401:
self.postNotificationOnMainThread(kAppTokenRefreshRequiredNotification)
// ...
}.resume()
Periodically API requests fail with a 401, which gets broadcast in the app as a notification, as many parties are interested in the fact the request is going to take a bit longer. Another API request is used to refresh an app token and then other API requests start working again.
This is the part I need help with: I then need to repeat the original API request following re-authorization.
Previously in Objective-C I might have used an NSInvocation object to package up the original method call, but this is not available in Swift and I'm therefore thinking there must be a more Swift-like approach. However I've not yet managed to get it figured out.
The best approach I've come up with so far is to:
put the arguments, along with a reference to the ServerController into a dictionary and pass them into the notification as the object: parameter
in the notification observer (after completing the re-authorization), pull the arguments out of the dictionary and manually call the same method again to repeat the request
This seems like a sub-optimal solution and there must be better way.