10

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:

  • The URL used by the session is valid.
  • All credentials are valid.
  • It fails instantly with a "cancelled" error, that simply did not happen before. I am not cancelling the task anywhere, so it's being cancelled by the system.
  • It also fails on Playgrounds with indefinite execution enabled. This is not limited to my tests.

Here's a list of things I have tried:

  • Because I suspect this is a bug, I first tried to clean my project, delete derived data, and reset all simulators. None of them worked.
  • Even went as far restarting my Mac...
  • Under the small suspicion that the upload task was getting deallocated due to it not having any strong pointers, and in turn calling cancel, I also rewrote authorize to return the task created by buildPOSTTask and assigned it to a variable in my test. The task was still getting cancelled.

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

  • Run it on a physical device. Currently downloading iOS 10 on an iPad as this is an iOS 10 project. EDIT: I just tried and it's not possible to do this.

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

Andy Ibanez
  • 11,317
  • 8
  • 63
  • 94
  • Right, sorry, guess that is important, haha. Yeah the test does have a waitForExpectationsWithTimeout call. I will edit my question to add that. I haven't tried it in an actual app yet (this is a framework I'm building, but an app that consumes will be built shortly afterwards), but it does fail in a Swift playground with indefinite execution enabled. – Andy Ibanez Jul 16 '16 at 00:00
  • Can you try: http://pastebin.com/B7nZFY1n Does it print the response? Gives an error? – Brandon Jul 16 '16 at 01:46
  • Thanks for your input Brandon. I ran it on a Playground and it worked fine. Printed the response as expected. – Andy Ibanez Jul 16 '16 at 01:50
  • Writing a test for the code you gave me is working fine as well. Something must be happening between my method calls. – Andy Ibanez Jul 16 '16 at 01:58
  • http://pastebin.com/ijshDH6Q Seems to be the same code you're using. I was able to deduce it based on your error code. – Brandon Jul 16 '16 at 02:26
  • I just went to `buildPOSTTask` and hard coded the StackOverflow URL there. So instead of `var request = URLRequest(url: fullURL)`, I had `var request = URLRequest(url: URL(string: "http://www.stackoverflow.com")!)`. It still failed. It also fails too soon, so it doesn't really look like it gets to make the request. I just tried doing it in the Playground as well, and also got cancelled there. – Andy Ibanez Jul 16 '16 at 02:32

3 Answers3

14

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
}
Andy Ibanez
  • 11,317
  • 8
  • 63
  • 94
0

Your server is broken..

tcp_connection_cancel 1
nw_socket_handle_socket_event Event mask: 0x4
nw_socket_handle_socket_event Socket received WRITE_CLOSE event
nw_endpoint_handler_cancel [1 anilist.co:443 ready resolver (satisfied)]
nw_endpoint_handler_cancel [1.1 104.28.1.44:443 ready socket-flow (satisfied)]
__nw_socket_service_writes_block_invoke sendmsg(fd 9, 31 bytes): socket has been closed
nw_endpoint_flow_protocol_error [1.1
104.28.1.44:443 cancelled socket-flow (null)] Socket protocol sent error: [32] Broken pipe
nw_endpoint_flow_protocol_disconnected [1.1 104.28.1.44:443 cancelled socket-flow (null)] Output protocol disconnected
nw_endpoint_handler_cancel [1.2 104.28.0.44:443 initial path (null)] 
nw_resolver_cancel_on_queue 0x60800010da40
[NWConcrete_tcp_connection dealloc] 1
[User Defaults] CFPrefsPlistSource<0x6180000f8700> (Domain: XIO.PrivateAPITest, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null)) is waiting for writes to complete so it can determine if new data is available

It waits infinitely for "writes" to complete..

It sends the request.. does the SSL handshake and gets no response. It times out and considers it a broken request..

class WTF : NSObject, URLSessionDelegate {

    var urlSession: URLSession!

    override init() {
        super.init()

        urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)

        var request = URLRequest(url: URL(string: "https://anilist.co/api/auth/access_token?client_secret=REMOVED&grant_type=authorization_code&redirect_uri=genericwebsitethatshouldntexist.bo&client_id=ibanez-hod6w&code=REMOVED")!)
        request.httpMethod = "POST"

        let data = try! JSONSerialization.data(withJSONObject: ["Test":"Test"], options: [])

        urlSession.uploadTask(with: request, from: data).resume()
    }

    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        completionHandler(.performDefaultHandling, nil)

    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {

    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: NSError?) {

    }

    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: (URLRequest?) -> Void) {

        completionHandler(request)

    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {

        completionHandler(.allow)

    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

    }
}
Brandon
  • 20,445
  • 9
  • 73
  • 162
  • Well like I said in the comments of the question, I have the same error when I replace the URL with any generic one. I replaced `fullURL` in `buildPOSTTask` with `http://www.stackoverflow.com` and `https://www.google.com` and the same thing is happening. The code you provided in this answer also finishes successfully in a Playground, and I'm able to see the response (for the Anilist URL). It doesn't look like the server is broken (I don't manage it BTW), and there's been no reports of people having troubles authorizing apps. This is confusing, I will try it in an actual app to see how it goes. – Andy Ibanez Jul 16 '16 at 16:52
  • Alright, just tested my whole code in a single view app. I get the same error, no matter if the URL is the AniList one or StatckOverflow or Google. It's definitely an error with my code. – Andy Ibanez Jul 16 '16 at 16:59
  • More info, the whole `is waiting for writes to complete so it can determine if new data is available` thing doesn't seem to have anything to do with the request itself. It seems to be part of some aggressive logging in the current beta releases. I was only able to find [this](https://forums.developer.apple.com/message/153159#153159), but you can see they are not even using URLSession or equivalent APIs. – Andy Ibanez Jul 16 '16 at 17:22
  • 1
    Went as far trying to run the code in an entirely new machine. Same error. Haha. Well at least I know my main computer is not acting funny. – Andy Ibanez Jul 17 '16 at 19:58
  • As of Xcode 8 Beta 3, we can officially discard the `is waiting for writes to complete so it can determine if new data is available` logging as meaningless. Thank you for your input though. Will try to figure out what's wrong. – Andy Ibanez Jul 18 '16 at 22:25
  • I'll also try to figure it out. I'm downloading the beta as well. Finished upgrading my device. – Brandon Jul 18 '16 at 22:44
  • I have just added a link to the project in the question, so running it as easy as opening the project in Xcode and doing a test. At this point I don't care about the output. If you see anything other than "Response is nil" in your console please let me know. Maybe, after all, there is a problem in my end that is not necessarily related to the computer. – Andy Ibanez Jul 18 '16 at 22:52
  • As a final attempt to try things, I also created a session to use a URLSession and a URLSessionUploadTask directly there. It also failed. – Andy Ibanez Jul 18 '16 at 22:53
0

Are you by any chance using a third party library such as Ensighten? I had the exact same problem in XCode 8 beta (works fine in XCode 7) and all of my blocks with nil parameters were causing crashes. Turns out it was the library doing some encoding causing the issue.

Fletcher
  • 131
  • 5