Request to AWS Lambda returning -999 URLSessionError on iOS but not Catalyst

I'm trying to solve a behavior difference between iOS and the respective Catalyst app with regards to pulling data from an AWS Lambda.


When I run the app on Mac OS X 10.15, the dataTaskPublisher that requests the URL completes successfully.


But when running on an iOS device, the dataTask is cancelled with an error (-999) that I believe is related to SSL restrictions that are more fully enforced in iOS 13.


I can't seem to find a specific pointer to the correct procedure -- any pointers would be greatly appreciated.


Thanks!

Replies

Error -999 is

NSURLErrorCancelled
error, and it’s hard to imagine how TLS restrictions could trigger that. Anyway, that doesn’t change my recommendation, which is to fire up CFNetwork diagnostic logging to see what it has to say on the matter.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

OK, that is a great next step. FWIW I did find this great page on App Transport Security


https://useyourloaf.com/blog/app-transport-security


that talked about ATS and showed me nscurl and curl, which provides proof (I think) that it is not ATS related - seems like it starts pulling down data.


The only other interesting data point is Safari on IOS will return data from the page, but not through the app. So perhaps the response is malformed. Here's what comes through on Mac OS X 10.15... and I'll post the diagnostics as soon as I can.


% curl -v https://[CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY]/v1/query1 10:29:49

* Trying 13.225.62.80...

* TCP_NODELAY set

* Connected to [CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY] (13.225.62.80) port 443 (#0)

* ALPN, offering h2

* ALPN, offering http/1.1

* successfully set certificate verify locations:

* CAfile: /etc/ssl/cert.pem

CApath: none

* TLSv1.2 (OUT), TLS handshake, Client hello (1):

* TLSv1.2 (IN), TLS handshake, Server hello (2):

* TLSv1.2 (IN), TLS handshake, Certificate (11):

* TLSv1.2 (IN), TLS handshake, Server key exchange (12):

* TLSv1.2 (IN), TLS handshake, Server finished (14):

* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):

* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):

* TLSv1.2 (OUT), TLS handshake, Finished (20):

* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):

* TLSv1.2 (IN), TLS handshake, Finished (20):

* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256

* ALPN, server accepted to use h2

* Server certificate:

* subject: CN=[CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY]

* start date: Dec 5 00:00:00 2019 GMT

* expire date: Jan 5 12:00:00 2021 GMT

* subjectAltName: host "[CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY]" matched cert's "[CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY]"

* issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon

* SSL certificate verify ok.

* Using HTTP2, server supports multi-use

* Connection state changed (HTTP/2 confirmed)

* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0

* Using Stream ID: 1 (easy handle 0x7fab7d800000)

> GET /v1/query1 HTTP/2

> Host: [CUSTOM_DOMAIN_FOR_AWS_API_GATEWAY]

> User-Agent: curl/7.64.1

> Accept: */*

>

* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!

< HTTP/2 200

< content-type: application/json

< content-length: 209277

< date: Thu, 05 Dec 2019 15:43:10 GMT

< x-amzn-requestid: 2c194c6c-56c5-4cf3-8428-01fc5071f404

< x-amz-apigw-id: EPLORHrfIAMFchg=

< x-amzn-trace-id: Root=1-5de9258e-08bf00449ae065c895fb60d8;Sampled=0

< x-cache: Miss from cloudfront

< via: 1.1 c9fc8eca0b2b3a083a77fd1cf662c1a9.cloudfront.net (CloudFront)

< x-amz-cf-pop: EWR53-C1

< x-amz-cf-id: o5emzx5iB7k9ioxa10nqGnnw0l-V78LKmv7p4bUWskd75uDJe4W4cg==

<

{"contents": [{"un_code": 44, "iso3": [lots more data here...]

Here is the CFNetworking log entries from running the app on device:


default11:08:25.881607-0500AM19CFNetwork Diagnostics [3:22] 11:08:25.881 {

LoaderWhatToDo: (null)

Request: <CFURL 0x280ea6280 [0x1c28a7cb0]>{string = https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506, encoding = 134217984, base = (null)}

CachePolicy: 0

WhatToDo: originload

CreateToNow: 0.00161s

} [3:22]

default11:08:25.882958-0500AM19CFNetwork Diagnostics [3:23] 11:08:25.882 {

AddCookies Continue: request GET https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506 HTTP/1.1

HTTPProtocol: Task: be87f10

} [3:23]

default11:08:25.883083-0500AM19CFNetwork Diagnostics [3:24] 11:08:25.882 {

HTTPCookieStorage::copyCookiesForURL: <CFHTTPCookieStorage 0x2804a0600 [0x2804a0610]>

Request URL: https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506

MainDocument URL: NONE

} [3:24]

default11:08:25.883220-0500AM19CFNetwork Diagnostics [3:25] 11:08:25.882 {

Response Error: (null)

Request: <NSMutableURLRequest: 0x2822b8670> { URL: https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506 }

Error: Error Domain=kCFErrorDomainCFNetwork Code=-1004 "(null)"

} [3:25]

default11:08:25.883301-0500AM19CFNetwork Diagnostics [3:26] 11:08:25.882 {

~HTTPProtocol: request GET https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506 HTTP/1.1

Request: <NSMutableURLRequest: 0x2822b8670> { URL: https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506 }

sent: 0

received: 0

cell sent: 0

cell received: 0

} [3:26]



and what it actually said in XCode - interesting that XCode gives -999, but the log gives -1004:


2019-12-05 11:08:25.879996-0500 AM19[6788:5003078] Task <3C084E99-159C-481C-937C-6996DD3BB48F>.<5> finished with error [-999] Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506, NSLocalizedDescription=cancelled, NSErrorFailingURLKey=https://[DOMAIN SNIPPED]/v1/query1?lon-se=-68.44681468234506&lat-nw=44.62472204130417&lon-nw=-94.01319352660852&lat-se=30.333997319474506}

One last response - I believe this is definitely my misunderstanding some Combine sample code.


This is a simplified version of the Combine-oriented sample code I was using:


func doCombine() {
       
        let myURL = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-20")

        let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!)
                        .map { $0.data }
                        .decode(type: PostmanResponse.self, decoder: JSONDecoder())
                        //.receive(on: RunLoop.main)  //have tried this with and without sending to main thread
                   
        let sink = remoteDataPublisher
                    .sink(receiveCompletion: { completion in
                            switch completion {
                                case .finished:
                                    break
                                case .failure(let anError):
                                    print("received error: ", anError)
                            }
                    }, receiveValue: { someValue in
                       print(".sink() received value \(someValue)")
                    })
    }


This was the non-Combine version:


func oldFashioned() {

        let url = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-20")!
       
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                fatalError("Error: \(error.localizedDescription)")
            }
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
                fatalError("Error: invalid HTTP response code")
            }
            guard let data = data else {
                fatalError("Error: missing response data")
            }

            do {
                let decoder = JSONDecoder()
                let posts = try decoder.decode(PostmanResponse.self, from: data)
                print(posts)
            }
            catch {
                print("Error: \(error.localizedDescription)")
            }
        }
        task.resume()
    }


Since it works fine the old fashioned way, I figured it's my misuse of the publisher and sink. What I found interesting is if i comment out the

.receive(on: RunLoop.main) , it cancels the request both on the Mac and IOS. So I'm now convinced there is a best practice I am ignoring somewhere.


If there's Apple sample code that does a better job of explaining this, I'm happy to take a look. But this is not AWS or Lambda related for sure.

The most common cause of unexpected cancellation in Combine is a failure to keep a reference to your cancellable (typically an

AnyCancellable
). In your first code snippet, you assign the cancellable to
sink
but then do nothing with that. When
sink
goes out of scope, Swift’s ARC releases the last reference to it and everything cancels.

Probably (-: I didn’t actually run your code to be sure.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"