How to further debug a simple HTTP request?

I'm making a simple HTTP request towards an API. I extracted it as a script, simple like this:

import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking†
#endif

let url = URL(string: "https://lab.magiconch.com/api/nbnhhsh/guess")!
var request = URLRequest(url: url)
request.httpMethod = "POST"

request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")

let bodyObject: [String : Any] = [
    "text": "google"
]
request.httpBody = try! JSONSerialization.data(withJSONObject: bodyObject)

URLSession.shared.dataTask(with: request) { data, _, error in
    if let data = data, let string = String(data: data, encoding: .utf8) {
        print(string)
    }

    if let error = error {
        print(error.localizedDescription)
    }
}.resume()

sleep(100)

While this works totally fine on my Mac, which prints this:

[{"name":"google","trans":["谷歌"]}]

it failed on my Linux server:

NSURLErrorDomain error -1001.

I'm pretty sure there's no connectivity issue because I tried cURL via command line and it works fine. I also tried configuring a SSH tunnel to use my Mac as a network proxy.

Both my server and my local machine are running Swift 5.6, the latest stable version.
While it failed on two of my AWS instances, there's seems no issue on my non-AWS server (All running Ubuntu Server LTS 20.04). So I don't consider it as a bug of FoundationNetworking Library. But I have no clue what to suspect.

The error does not give much useful information. So I want to know how to solve this issue, or how to debug it further.

In the NSURLError header, it seems this error number means the request timed out. My blind guess is the call to the sleep function is interfering. Have you tried using something else to prolong your program, like running the the run loop until it receives input?

1 Like

+1 on this: nothing has started the main queue here, and it's entirely possible that the Linux implementation relies on it.

1 Like

Thanks for your reply.

I tried running a loop or using DispatchSemaphore. Both behave exactly the same as before.

Besides, this failure only occurs on several of my machines so I don't think using sleep() is the cause.

Can you please share the code you used for the run loop?

Replace sleep(100) with RunLoop.main.run().

Yes. Just like @Peter-Schorn said.

import Foundation

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

let url = URL(string: "https://lab.magiconch.com/api/nbnhhsh/guess")!
var request = URLRequest(url: url)
request.httpMethod = "POST"

request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")

let bodyObject: [String : Any] = [
    "text": "google"
]
request.httpBody = try! JSONSerialization.data(withJSONObject: bodyObject)

URLSession.shared.dataTask(with: request) { data, response, error in
    if let data = data {
        print("Data recieved.")
        if let string = String(data: data, encoding: .utf8) {
            print(string)
        }
    }

    if let response = response {
        let httpResponse = response as! HTTPURLResponse
        print(httpResponse.allHeaderFields)
    }

    if let error = error {
        print(error.localizedDescription)
    }

}.resume()

RunLoop.main.run()

Everything remains the same except the last line.

And the result is still The operation could not be completed. (NSURLErrorDomain error -1001.).

How long does it take in total for the program to execute? URLSession does have a default timeout interval of 60 seconds.

The program takes exactly 60 seconds to execute.

I also created a custom URLSessionConfiguration and change the timeoutIntervalForRequest to other values. The program did end after that time.

Well, there's your problem.

Thanks a lot but I still don't get it. Could you explain a bit more? How could I find the reason of this timeout.

Are there some configurations that I have to tweak? Cuz I can get the data via cURL, I don't think it the network issue.

I also found that if I run this program again and again, occasionally it works.

Same code, slightly modified (uses dispatchMain() + exit(0) in the completion), works just fine on macOS, so I'd guess it's a FoundationNetworking bug. You'll want to run a network proxy debug tool like Proxyman (or Wireshark, though that's low level) to investigate the request and response and see what's happening on the network. The fact you're getting occasional responses is rather strange.

I have started doing some Swift stuff on linux. I fire off a very large number of small requests to a very fast server, and occasionally run into the -1001 error (timeout I presume). The vast majority of my requests succeed (indeed when the program starts, all operations work for a while) but sooner or late, I hit this error after a long delay. It looks like sometimes the server in question receives the request and responds and my client doesn't see it, while other times my client doesn't even manage to fire off the request to the server (I have instrument the server).

If anyone has more info about linux bugs in FoundationNetworking, or rather, any ideas/workarounds, I'd love to hear about it. I'm seriously considering trying another network package but I'd really hate to go to that trouble.

I'm actually wondering if it's maybe a threading issue, from running strace it seems like each time a dataTask is created from URLSession it's starting a new thread to do it, but I could easily be wrong about that. The fact that sometimes the server sees the request and it hangs vs sometimes the reqest just doesn't get out at all is perplexing.

I gave the SwiftNIO stuff a chance to build in my docker linux container, but it spewed so many errors I immediately gave up on that route.

I see strong evidence that if you do multiple simultaneous requests in different threads, that's what triggers this condition (at least for me). So I infer there is something that just isn't thread safe.

SOLVED (my case): For anyone on Linux who hits this, here's what I've found out. On macOS/iOS, the URLSession object appears thread safe, in the sense that it is ok to call urlSession.dataTask() from different threads at the same time.

This is not the case on Linux. Rather than creating a URLSession and using it for all my requests, I changed my code to create a new URLSession for each request, and the blocking behavior immediately went away. I infer there is some shared mutable state in URLSession that is having issues. FYI my URLSession uses the ephemeral configuration, because I don't want any caching etc.

FYI, Apple specifically says not to do that since URLSession is rather heavy weight. But if you're ephemeral anyway it shouldn't break anything functionally, just be slow. I suggest you limit that behavior to Linux, where you could also try to serialize access to your URLSession, perhaps by wrapping it in an actor or another thread-safety abstraction.

You could also try using Alamofire, which serializes access to the URLSession by default. It builds on Linux, though it's untested due to large swaths of missing URLSession functionality.

Yes, I have only changed the behavior for Linux. I guess I can check experimentally, I figured that having two (or more) active dataTask objects going at once from the same URLSession would break things, which is why I opted to create a session for each request. But perhaps I can lock the call to produce the dataTask, and once the dataTask(s) are running they’ll be OK. I should try that.

I guess I could run the whole thing single threaded, with and without creating a URLSession per request and actually measure the impact. Is creating a URLSession that expensive compared with the time you spend waiting for the request anyway? I should mention since this is Linux, and given where I work, all the machines that will use this code are 16-core (or 32 if you count hyperthreading) beasts...

You can try the lock around the URLSession. Alamofire simply ensures all accesses of the internal URLSession instance happens on the same queue, which should have the same effect.

Depending on how many requests you have in flight simultaneously, you could seem greater memory and CPU use by using a URLSession per request. Whether that's relevant is up to you.

I don't see a particular behaviour being documented. Although common sense and the presence of, say, httpMaximumConnectionsPerHost, suggests it should be possible to call several dataTasks on the same URLSession concurrently, and obviously there's ton of user code that makes that assumption (on Apple platforms). Linux implementation deserves a fix. Until it is fixed, as a workaround I'd create some wrapper "MyUrlSession" which maintains a pool of real URLSession up to a certain number, like 100. And if user code assumes a working httpMaximumConnectionsPerHost logic - the wrapper would have to implement this logic.

I checked and serializing calls to create the dataTask (and even call resume) don’t help. you basically need a completely separate URLSession per request going (on different threads) on Linux.

Note that the Apple documentation explicitly states that the way I use URLSession on iOS/macOS/tvOS is fine, i.e. that URLSession is threadsafe. It just isn’t on Linux which is the bug that needs fixing.

I don’t see any big performance hit from creating/discarding URLSessions based on ephemeral. It’s not ideal obviously, but for now it’s a perfectly functional work around (for me) on Linux.

1 Like