I have created a POC to implement various types of download in SwiftUI. The project is available at https://github.com/curia-damiano/SwiftUIDownloader. This project has always worked, but for some reasons it doesn't work anymore. The app downloads files like https://speed.hetzner.de/100MB.bin; if I try to another files, for example https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_5MG.mp3, it still works perfectly.
The error that I get is:
2023-04-16 18:42:20.665139+0200 SwiftUIDownloader[10442:425026] Task <54E67F7F-7822-4429-9943-12DA94DDCB27>.<1> HTTP load failed, 446/0 bytes (error code: -1005 [4:-4])
2023-04-16 18:42:20.666751+0200 SwiftUIDownloader[10442:425023] Task <54E67F7F-7822-4429-9943-12DA94DDCB27>.<1> finished with error [-1005] Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost." UserInfo={_kCFStreamErrorCodeKey=-4, NSUnderlyingError=0x6000015a3930 {Error Domain=kCFErrorDomainCFNetwork Code=-1005 "(null)" UserInfo={NSErrorPeerAddressKey=<CFData 0x60000382c730 [0x1bbb34418]>{length = 16, capacity = 16, bytes = 0x100201bb58c6f8fe0000000000000000}, _kCFStreamErrorCodeKey=-4, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <54E67F7F-7822-4429-9943-12DA94DDCB27>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=( "LocalDataTask <54E67F7F-7822-4429-9943-12DA94DDCB27>.<1>" ), NSLocalizedDescription=The network connection was lost., NSErrorFailingURLStringKey=https://speed.hetzner.de/100MB.bin, NSErrorFailingURLKey=https://speed.hetzner.de/100MB.bin, _kCFStreamErrorDomainKey=4}
I've also tried to troubleshoot the UrlSession, but then from the Console app I don't get any clear information about the cause of the error. I've also tried the app on old iPhone that had old builds of the app, and they have this error now - so I am sure that it is something that has changed on the server.
Can anyone please help me in understanding what I can change to make the download to work again?
My first step in testing URLSession
is to point curl
at the resource to see if it has problems. Here’s what I saw for your latest URL:
% curl -D /dev/stderr 'https://www.backade.com/22Z9H358/SZCPXRW/?uid=2297'
HTTP/1.1 204 No Content
Server: nginx
Date: Mon, 29 May 2023 08:17:49 GMT
Accept-Ch: Sec-Ch-Ua-Platform-Version
Vary: Origin
X-Eflow-Request-Id: dba0091f-73eb-444e-9b9f-55e07b1eb18e
What are you expecting to happen here? Because the server has clearly indicated, with the 204 No Content
status, that there’s nothing at this specific URL.
If I plug this URL into a simple command-line tool project [1], I get this:
will start task
did start task
task finished with status 204, bytes 0
which is exactly what I’d expect.
I then repeated with your other URLs. For https://example.com/ I got this:
will start task
did start task
task finished with status 200, bytes 1256
So far so good. However, for both of the hetzner.de
URLs I got this:
will start task
did start task
… scary logging output elided …
task did fail, error NSURLErrorDomain / -1005
Presumably that’s the problem you’re trying to fix.
Now this works just fine with curl
:
% curl -D /dev/stderr -O 'https://speed.hetzner.de/100MB.bin'
…
Server: nginx
Date: Mon, 29 May 2023 08:20:35 GMT
Content-Type: application/octet-stream
Content-Length: 104857600
Last-Modified: Tue, 08 Oct 2013 11:48:13 GMT
Connection: keep-alive
ETag: "5253f0fd-6400000"
Strict-Transport-Security: max-age=15768000; includeSubDomains
Accept-Ranges: bytes
100 100M 100 100M 0 0 4249k 0 0:00:24 0:00:24 --:--:-- 4469k
which is definitely curious.
Looking at the scary logging that I elided, I see this:
2023-05-29 09:25:48.270617+0100 xxst[8350:12361501] [tcp] tcp_input [C1.1.1.1:3] flags=[R] seq=2724499340, ack=0, win=0 state=LAST_ACK rcv_nxt=2724499340, snd_una=3863174206
The flags=[R]
means that the server closed the underlying TCP connection. Looking at a packet trace I see exactly that
09:30:28.378792 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [S], …
09:30:28.422487 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [S.], …
09:30:28.423462 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [.], …
09:30:28.423462 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [P.], …
09:30:28.475682 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [.], …
09:30:28.475683 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [.], …
09:30:28.475683 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [.], …
09:30:28.475684 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [P.], …
09:30:28.476578 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [.], …
09:30:28.480705 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [P.], …
09:30:28.526014 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [P.], …
09:30:28.527928 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [.], …
09:30:28.529610 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [P.], …
09:30:28.579270 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [F.], …
09:30:28.579434 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [.], …
09:30:28.579789 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [P.], …
09:30:28.580783 IP 192.168.1.71.55697 > 88.198.248.254.443: Flags [F.], …
09:30:28.582996 IP 192.168.1.71.55698 > 88.198.248.254.443: Flags [S], …
09:30:28.626226 IP 88.198.248.254.443 > 192.168.1.71.55697: Flags [R], …
09:30:28.378792
is the start of the TCP connection, with my Mac sending a SYN to the server. That handshake completes successfully and then we start the TLS dance. That seems to be doing OK until 09:30:28.579270
, when the server initiates a disconnect. It seems that something about the client’s request is causing the server to drop the client.
The thing that seems to have triggered that is the packet at 09:30:28.529610
. Looking at that it seems to be the HTTP request going out to the server (that is, the TLS handshake is finished and the client is now sending the HTTP request over TLS). Something about that request is causing the server to drop the connection.
Debugging problems like this is a pain because the issue is on the server side. I talk about this in some detail in Debugging HTTP Server-Side Errors. Fortunately, we have a working client, curl
, so we can use the process described in the Compare Against a Working Client section.
To investigate this further I put my code into a trivial app and then used the Network instrument template (Analyzing HTTP Traffic with Instruments) to capture the headers on its outgoing request. Here’s what I saw:
User-Agent: xxsm/1 CFNetwork/1406.0.4 Darwin/22.4.0
Accept: */*
Accept-Language: en-GB,en;q=0.9
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
Host: speed.hetzner.de
Note xxsm
is the name of my test app. I use very short names because I create a lot of test apps.
Nothing about this looks remotely strange, which caused me to suspect the user agent string. To test that I ran curl
with my test app’s user agent string:
% curl -D /dev/stderr -O -H 'User-Agent: xxsm/1 CFNetwork/1406.0.4 Darwin/22.4.0' 'https://speed.hetzner.de/100MB.bin'
…
curl: (52) Empty reply from server
Hmmm, that’s not good. Retrying with a dummy user agent string works:
% curl -D /dev/stderr -O -H 'User-Agent: foo' 'https://speed.hetzner.de/100MB.bin'
So I tweaked my test app to use that dummy string:
var request = … as before …
request.setValue("foo", forHTTPHeaderField: "User-Agent")
and now it works too:
will start task
did start task
task finished with status 200, bytes 104857600
In summary, this specific server doesn’t like the default user agent string being generated by URLSession
. I’ve no idea why; to work that out, you’d have to ask the folks who run the server. Regardless, you can work around this by applying your own user agent.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] Using this code:
import Foundation
func main() {
print("will start task")
let url = URL(string: "https://www.backade.com/22Z9H358/SZCPXRW/?uid=2297")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error as NSError? {
print("task did fail, error \(error.domain) / \(error.code)")
return
}
let response = response as! HTTPURLResponse
let data = data!
print("task finished with status \(response.statusCode), bytes \(data.count)")
}.resume()
print("did start task")
dispatchMain()
}
main()