iosnsurlsessionswift3xcode8nsurlsessionuploadtask

URLSessionUploadTask getting automatically cancelled instantly


I'm having this weird issue in which a newly created URLSessionUploadTask gets cancelled instantly. I'm not sure if it's a bug with the current beta of Xcode 8.

I suspect it might be a bug because the code I'm about to post ran fine exactly once. No changes were made to it afterwards and then it simply stopped working. Yes, it literally ran once, and then it stopped working. I will post the error near the end.

I will post the code below, but first I will summarize how the logic here works.

My test, or user-exposed API (IE for use in Playgrounds or directly on apps), calls the authorize method. This authorize method will in turn call buildPOSTTask, which will construct a valid URL and return a URLSessionUploadTask to be used by the authorize method.

With that said, the code is below:

The session:

internal let urlSession = URLSession(configuration: .default)

Function to create an upload task:

internal func buildPOSTTask(onURLSession urlSession: URLSession, appendingPath path: String, withPostParameters postParams: [String : String]?, getParameters getParams: [String : String]?, httpHeaders: [String : String]?, completionHandler completion: URLSessionUploadTaskCompletionHandler) -> URLSessionUploadTask {
    let fullURL: URL
    if let gets = getParams {
        fullURL = buildURL(appendingPath: path, withGetParameters: gets)
    } else {
        fullURL = URL(string: path, relativeTo: baseURL)!
    }

    var request = URLRequest(url: fullURL)
    request.httpMethod = "POST"

    var postParameters: Data? = nil

    if let posts = postParams {
        do {
            postParameters = try JSONSerialization.data(withJSONObject: posts, options: [])
        } catch let error as NSError {
            fatalError("[\(#function) \(#line)]: Could not build POST task: \(error.localizedDescription)")
        }
    }

    let postTask = urlSession.uploadTask(with: request, from: postParameters, completionHandler: completion)
    return postTask
}

The authentication function, which uses a task created by the above function:

    public func authorize(withCode code: String?, completion: AccessTokenExchangeCompletionHandler) {

// I have removed a lot of irrelevant code here, such as the dictionary building code, to make this snippet shorter.

        let obtainTokenTask = buildPOSTTask(onURLSession: self.urlSession, appendingPath: "auth/access_token", withPostParameters: nil, getParameters: body, httpHeaders: nil) { (data, response, error) in
            if let err = error {
                completion(error: err)
            } else {
                print("Response is \(response)")
                completion(error: nil)
            }
        }

        obtainTokenTask.resume()
    }

I caught this error in a test:

let testUser = Anilist(grantType: grant, name: "Test Session")

let exp = expectation(withDescription: "Waiting for authorization")

testUser.authorize(withCode: "a valid code") { (error) in
    if let er = error {
        XCTFail("Authentication error: \(er.localizedDescription)")
    }
    exp.fulfill()
}
self.waitForExpectations(withTimeout: 5) { (err) in
    if let error = err {
        XCTFail(error.localizedDescription)
    }
}

It always fails instantly with this error:

Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=https://anilist.co/api/auth/access_token?client_secret=REMOVED&grant_type=authorization_code&redirect_uri=genericwebsitethatshouldntexist.bo&client_id=ibanez-hod6w&code=REMOVED, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=https://anilist.co/api/auth/access_token?client_secret=REMOVED&grant_type=authorization_code&redirect_uri=genericwebsitethatshouldntexist.bo&client_id=ibanez-hod6w&code=REMOVED}

Here's a few things to keep in mind:

Here's a list of things I have tried:

Things I have yet to try (but I will accept any other ideas as I work through these):

I'm out of ideas of what to try. The generated logs don't seem to have any useful info.

EDIT:

I have decided to just post the entire project here. The thing will be open source anyway when it is finished, and the API credentials I got are for a test app.

ALCKit


Solution

  • After struggling non-stop with this for 6 days, and after googling non-stop for a solution, I'm really happy to say I have finally figured it out.

    Turns out that, for whatever mysterious reason, the from: parameter in uploadTask(with:from:completionHandler) cannot be nil. Despite the fact that the parameter is marked as an optional Data, it gets cancelled instantly when it is missing. This is probably a bug on Apple's side, and I opened a bug when I couldn't get this to work, so I will update my bug report with this new information.

    With that said, everything I had to do was to update my buildPOSTTask method to account for the possibility of the passed dictionary to be nil. With that in place, it works fine now:

    internal func buildPOSTTask(onURLSession urlSession: URLSession, appendingPath path: String, withPostParameters postParams: [String : String]?, getParameters getParams: [String : String]?, httpHeaders: [String : String]?, completionHandler completion: URLSessionUploadTaskCompletionHandler) -> URLSessionUploadTask {
        let fullURL: URL
        if let gets = getParams {
            fullURL = buildURL(appendingPath: path, withGetParameters: gets)
        } else {
            fullURL = URL(string: path, relativeTo: baseURL)!
        }
    
        var request = URLRequest(url: fullURL)
        request.httpMethod = "POST"
    
        var postParameters: Data
    
        if let posts = postParams {
            do {
                postParameters = try JSONSerialization.data(withJSONObject: posts, options: [])
            } catch let error as NSError {
                fatalError("[\(#function) \(#line)]: Could not build POST task: \(error.localizedDescription)")
            }
        } else {
            postParameters = Data()
        }
    
        let postTask = urlSession.uploadTask(with: request, from: postParameters, completionHandler: completion)
        return postTask
    }