swiftscheduled-tasksnsurlsessionurlsessionnsurlsessiondatatask

Getting nil data when try to cancel a task and restart it again Swift


What I am trying to do:
Sometimes my URLSession call request takes a too long time to give a response back. That's why I am trying to call the call request again if it doesn't give a response back within 60 seconds. After 60 seconds it will cancel the call request, then give an alert to the user to try again. When the user taps on the try again alert button it will call the call request again from the beginning.

How I tried:
I declare a global variable for the session task like this:

private weak var IAPTask: URLSessionTask?

This is my call request function:

Code 1

func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool, _ error: Error?) -> ()) {
    let receiptFileURL = Bundle.main.appStoreReceiptURL
    guard let receiptData = try? Data(contentsOf: receiptFileURL!) else {
        //This is the First launch app VC pointer call
        completion(false, nil)
        return
    }
    let recieptString = receiptData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
    let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString as AnyObject, "password" : AppSpecificSharedSecret as AnyObject]

    do {
        let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
        let storeURL = URL(string: self.verifyReceiptURL)!
        var storeRequest = URLRequest(url: storeURL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = requestData
        let session = URLSession(configuration: URLSessionConfiguration.default)
        let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
            do {
                if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
                    if let latestInfoReceiptObjects = self?.getLatestInfoReceiptObjects(jsonResponse: jsonResponse) {
                        self?.getCurrentTimeFromServer(completionHandler: { currentDateFromServer in
                            let purchaseStatus = self?.isPurchaseActive(currentDateFromServer: currentDateFromServer, latestReceiptInfoArray: latestInfoReceiptObjects)
                            completion(purchaseStatus!, nil)
                        })
                    }
                }
            } catch let parseError {
                completion(false, parseError)
            }
        })
        task.resume()
        self.IAPTask = task
    } catch let parseError {
        completion(false, parseError)
    }
}

I am calling this request after every 60 seconds with a timer. But whenever I try to call it for a second time, I am getting nil value for data.

let task = session.dataTask(with: storeRequest, completionHandler: { [weak self] (data, response, error) in
    //Getting data = nil here for second time 
})

Let me show how I am canceling the global variable step by step.

First calling this where I am setting the timer and call for the call request. If I get the response within 10 seconds (For testing purposes I set it as 10), I am canceling the timer and the call request task and doing further procedures. :

Code 2

func IAPResponseCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
    let infoDic: [String : String] = ["IAPReceiptValidationFrom" : iapReceiptValidationFrom.rawValue]
    self.IAPTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(self.IAPTimerAction), userInfo: infoDic, repeats: false)
    IAPStatusCheck(iapReceiptValidationFrom: iapReceiptValidationFrom) { isSuccessful in
        if isSuccessful == true {
            self.IAPTimer.invalidate()
            self.IAPTask?.cancel()
            self.getTopVisibleViewController { topViewController in
                if let viewController = topViewController {
                    viewController.dismiss(animated: true, completion: nil)
                    self.hideActivityIndicator()
                }
            }
        }
    }
}  

This is the completion where I call the call request. I set a true value for the completion if it just gives me the response.

Code 3

func IAPStatusCheck(iapReceiptValidationFrom: IAPReceiptValidationFrom, complition: @escaping (_ isSuccessful: Bool)->()) {
    receiptValidation() { isPurchaseSchemeActive, error in
        if let err = error {
            self.onBuyProductHandler?(.failure(err))
        } else {
            self.onBuyProductHandler?(.success(isPurchaseSchemeActive))
        }
        complition(true)
    }
}

This is the timer action from where I am invalidating the timer and canceling the task call request and then showing the pop up alert to the user:

Code 4

@objc func IAPTimerAction(sender: Timer) {
    if let dic = (sender.userInfo)! as? Dictionary<String, String> {
        let val = dic["IAPReceiptValidationFrom"]!
        let possibleType = IAPReceiptValidationFrom(rawValue: val)
        self.IAPTimer.invalidate()
        self.IAPTask?.cancel()
        self.showAlertForRetryIAP(iapReceiptValidationFrom: possibleType!)
    }
}

And finally, call the same initial response check function in the alert "Try again" action.

Code 5

func showAlertForRetryIAP(iapReceiptValidationFrom: IAPReceiptValidationFrom) {
    DispatchQueue.main.async {
        let alertVC = UIAlertController(title: "Time Out!" , message: "Apple server seems busy. Please wait or try again.", preferredStyle: UIAlertController.Style.alert)
        alertVC.view.tintColor = UIColor.black
        let okAction = UIAlertAction(title: "Try again", style: UIAlertAction.Style.cancel) { (alert) in
            self.showActivityIndicator()
            self.IAPResponseCheck(iapReceiptValidationFrom: iapReceiptValidationFrom)
        }
        alertVC.addAction(okAction)
        DispatchQueue.main.async {
            self.getTopVisibleViewController { topViewController in
                if let viewController = topViewController {
                    var presentVC = viewController
                    while let next = presentVC.presentedViewController {
                        presentVC = next
                    }
                    presentVC.present(alertVC, animated: true, completion: nil)
                }
            }
        }
    }
}

This is the response I am getting:

▿ Optional<NSURLResponse>
  - some : <NSHTTPURLResponse: 0x281d07680> { URL: https://sandbox.itunes.apple.com/verifyReceipt } { Status Code: 200, Headers {
    Connection =     (
        "keep-alive"
    );
    "Content-Type" =     (
        "application/json"
    );
    Date =     (
        "Sun, 27 Feb 2022 17:28:17 GMT"
    );
    Server =     (
        "daiquiri/3.0.0"
    );
    "Strict-Transport-Security" =     (
        "max-age=31536000; includeSubDomains"
    );
    "Transfer-Encoding" =     (
        Identity
    );
    "apple-originating-system" =     (
        CommerceGateway
    );
    "apple-seq" =     (
        "0.0"
    );
    "apple-timing-app" =     (
        "154 ms"
    );
    "apple-tk" =     (
        false
    );
    b3 =     (
        "48d6c45fe76eb9d8445d83e518f01866-c8bfb32d2a7305e2"
    );
    "x-apple-jingle-correlation-key" =     (
        JDLMIX7HN245QRC5QPSRR4AYMY
    );
    "x-apple-request-uuid" =     (
        "48d6c45f-e76e-b9d8-445d-83e518f01866"
    );
    "x-b3-spanid" =     (
        c8bfb32d2a7305e2
    );
    "x-b3-traceid" =     (
        48d6c45fe76eb9d8445d83e518f01866
    );
    "x-daiquiri-instance" =     (
        "daiquiri:45824002:st44p00it-hyhk15104701:7987:22RELEASE11:daiquiri-amp-commerce-clients-ext-001-st"
    );
    "x-responding-instance" =     (
        "CommerceGateway:020115:::"
    );
} }

And this is the error:

▿ Optional<Error>
  - some : Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://sandbox.itunes.apple.com/verifyReceipt, NSErrorFailingURLKey=https://sandbox.itunes.apple.com/verifyReceipt, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <17863FCF-EC4C-43CC-B408-81EC19B04ED7>.<1>, NSLocalizedDescription=cancelled}

Solution

  • It seems that the way you reinitialize the URLSessionTask looks fine to me but I think there is something when cancelling a task and invalidating the timer seems to happen twice as you mentioned in code 2 and code 4 so it is a little tricky to debug.

    From the error it seems like a race condition (I could be wrong) type situation where while you are initializing a new URLSession the url session gets cancelled.

    What I can offer is a simpler alternative if you would like to try:

    When creating your URLRequest, instead of using a timer, use the timeoutInterval

    storeRequest.httpMethod = "POST" 
    storeRequest.httpBody = requestData
    
    // add this
    storeRequest.timeoutInterval = timeOutInterval
    

    Then inside your data task, handler you could check if you encounter the error due to a timeout:

    let task = session.dataTask(with: storeRequest) { [weak self] (data, response, error) in
        
        do {
            
            if let error = error
            {
                // This checks if the request timed out based on your interval
                if (error as? URLError)?.code == .timedOut
                {
                    // retry your request
                    self?.receiptValidation(completion: completion)
                }
            }
    

    The full mechanism for retrying seems to become a lot more simpler in my opinion and you don't need to manage a URLSessionTask object

    This is the full code:

    private func receiptValidation(completion: @escaping(_ isPurchaseSchemeActive: Bool,
                                                         _ error: Error?) -> ())
    {
        // all your initial work to prepare your data and jsonDict data
        // goes here
        
        do {
            let requestData = try JSONSerialization.data(withJSONObject: jsonDict!,
                                                         options: .prettyPrinted)
            
            let storeURL = URL(string: self.verifyReceiptURL)!
            var storeRequest = URLRequest(url: storeURL)
            storeRequest.httpMethod = "POST"
            storeRequest.httpBody = requestData
            storeRequest.timeoutInterval = timeOutInterval
            
            let session = URLSession(configuration: .default)
            
            let task = session.dataTask(with: storeRequest)
            { [weak self] (data, response, error) in
                
                do {
                    
                    if let error = error
                    {
                        // Error due to a timeout
                        if (error as? URLError)?.code == .timedOut
                        {
                            // retry your request
                            self?.receiptValidation(completion: completion)
                        }
                        
                        // some other error
                    }
                    
                    if let data = data,
                       let jsonResponse
                        = try JSONSerialization.jsonObject(with: data,
                                                           options: .mutableContainers) as? NSDictionary
                    {
                        // do your work or call completion handler
                        print("JSON: \(jsonResponse)")
                    }
                }
                catch
                {
                    print("Response error: \(error)")
                }
            }
            
            task.resume()
        }
        catch
        {
            print("Request creation error: \(error)")
        }
    }
    

    I am retrying the request immediately when I figure out the request timed out, however you would show your alert instead to ask the user to retry.

    Once they hit retry, all you need to do is call your function again receiptValidation(completion: completion) so probably the completion closure is what needs to be stored so that receiptValidation can be relaunched again.

    I know this does not exactly find the error in your code but have a look if this could help with your use case and simplify things ?