Post not yet marked as solved
I'm investigating some reported connectivity issues from users of our iOS app. The app is failing to load/refresh data for multiple minutes at a time, despite other apps, Safari, etc. working fine. We are investigating this at various layers in the pipeline (i.e. server-side, network), but I'm taking a look at what we know or can find out on the client.I'm sure a lot of these kinds of issues get posted which end up being caused by transient network issues but I have a few specific questions about how I can find out more, and some questions around the behaviour of URLSession, so hopefully there are some people qualified to answer (BatEskimo-signal activated).
packet trace or gtfo 🙂
Unfortunately we've been unable to get a network/packet trace as this requires reproducing the issue locally which we haven't been able to do.Device logs show what look like typical timeout errors, occurring 60 seconds after initiating the requests:
Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x280764ae0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=https://REDACTED, NSErrorFailingURLKey=https://REDACTED, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.})
The app is trying to send multiple requests over the minutes things are not working, and they are all failing with the same timeout error after 60 seconds. We've had users give up after 5 minutes because nothing is working. This is despite them having a good cellular or wifi connection and with other apps and Safari working. The users who have reported this so far are on iOS 12.We are using a single URLSession for all our requests, created when the app starts. It's pretty vanilla: the config is a URLSessionConfiguration.default but with a custom User-Agent to override the default one, added via httpAdditionalHeaders. All our requests hit the same https hostname, and they are all POSTs.Now the interesting part is that we have a separate health check request we send occasionally which sends a POST to exactly the same end point as normal requests, and we are seeing this succeed during the periods when regular requests are timing out. One difference with the ping check is that we use a different URLSession instance on the client. This URLSession is also created on startup, and uses the same configuration. The only difference is a delegate that we use to do some cert pinning and report any certificate mismatch from what we expect.We do have DNS load balancing on our end point, so different connections can end up hitting a different IP.So there are a few initial thoughts and questions I have:The failing requests could be going to a different IP than the successful health check ones, and a specific server could be bad in some way. Is there a way to log the resolved IP address that a particular URLSession task used, at the point of receiving the error? Googling and looking in the docs doesn't show an obvious way to get this information. I imagine since URLSession can maintain a pool of connections to the same host, and there can be redirects during a request, that this is difficult to expose "nicely" via the API. We can obviously do this with local profiling but we would like to add telemetry to gather this data in the wild if possible.Is it possible the "bad" URLSession is reusing a stale/dead persistent (keep-alive) connection, and everything on that socket is just timing out? What is the behaviour of connection reuse in these situations and under what circumstances will URLSession open a new connection? How long will it reuse a connection for? Will it continue reusing a connection even when requests are failing with timeout errors, even for multiple minutes?Is there a way to log exactly where in the lifetime of the request the URLSession task got to before it timed out? i.e. did it even resolve DNS? Did it connect at all? Did it finish the TLS handshake? Did it send headers? Did it receive anything at all? There is the NSURLSessionTaskMetrics API but it doesn't look like there's an easy way to correlate an event from urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) to a particular data task / request, so we'd have to log everything (maybe checking if response is null to detect an incomplete load) and correlate later.Some docs (e.g. "Technical Q&A QA1941" which I won't link because this post will be put in a moderator queue) talk about some retry behaviour in URLSession for idempotent (e.g. GET) vs. non-idempotent (e.g. POST) requests, at least for "The network connection was lost" errors. Is there a similar or related behaviour for timeouts, or when a connection looks dead? If this is some transient network issue, would GET requests behave better in such situations when stuff is timing out? There are reasons we are using POST but it would be interesting to know more about how differently idempotent requests are treatedThanks in advance
Post not yet marked as solved
Hi there. I'm doing some initial prototyping of using URLSession's connection upgrade mechanism to stream some arbitrary data back and forth between a client and server. Ultimately we're using this to get MQTT data through http proxies, with additional websocket framing on top because nginx has some built in support for that. The specific framing/protocol is kind of an implementation detail for the purposes of this question though; I'm just including for a bit of background.The two main references I'm using as a starting point here are the WWDC video from a few years back where the connection upgrade stuff was first introduced, https://developer.apple.com/videos/play/wwdc2015/711/, which talks specifically about upgrading to a stream task, and https://developer.apple.com/documentation/foundation/nsurlsessionstreamtask which links specifically to the web sockets and tls-upgrade rfcs.I have a dummy iOS app (which I can attach if required) which kind of works, but I'm running into an issue with losing some of the early bytes after upgrading the connection.What I'm doing right now:* Creating a URLSession data task, with httpShouldUsePipelining turned off on the session configuration* Setting a few headers so the server will recognise this and upgrade the connection on its end (e.g. "Upgrade: websocket", and a few others)* Calling resume() on the data task to kick off the initial request* Handing the urlSession(_:dataTask:didReceive:completionHandler:) to handle the point we get the initial http response back, and returning .becomeStream to upgrade to a stream task* Handling the subsequent call to urlSession(_:dataTask:didBecome:) which gives me the URLSessionStreamTask to read/write from.* Immediately calling read on the stream task with a minimum of 1 byte and a large maxThe server is sending a 101 upgrade response which ultimate is justHTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: UpgradeThough I don't believe URLSession really cares about the specifics of this.After this http response, the server is immediately writing some websocket data down to the client. This seems to be causing a problem though: these first early bytes are getting dropped somewhere and the read() call on the stream task is never completing. Though this is inconsistent - sometimes a few of the trailing bytes will get through, sometimes not.If I use netcat as the server and paste in the http response myself, wait a second, then start typing some stuff, the bytes are not missed and the stream read() calls return all the data as expected.So it seems like some of the first bytes after the HTTP response are getting swallowed up. I thought perhaps URLSession was expecting a body as part of the initial data task response and swallowing the bytes up for that reason, but if I simulate a response with a Content-Length: 1 and a dummy byte before writing the websocket data, this doesn't help and we still miss stuff.So a couple of questions:* Fundamentally, is what I'm doing reasonable and supported?* Any idea why I might be missing these first few bytes? It seems like a timing issue of some sort with the upgrade mechanism.
Post not yet marked as solved
Radar: 41773389 (submitted by a colleague, reposting here)Summary:Archiving a project that includes a Swift static library referencing ObjC types within system frameworks (e.g. NSObject, NSURLSessionDelegate) causes udefined symbol errors (e.g. Undefined symbols for architecture arm64: "l003", referenced from:...) when linking in Xcode 10 beta.Steps to Reproduce: Create a new Xcode project (sample attached) Create a new (Swift) static library (either .a or .framework wrapper with Mach-O Type set to Static Library) Ensure the main application target links in the static library Within the static library attempt to subclass or conform to an ObjC system framwork type (e.g. NSObject, NSURLSessionDelegate) Archive the projectExpected Results:Project should archive successfully (like it did when using Xcode 9)Actual Results:A linker error occurrs:Undefined symbols for architecture arm64: "l003", referenced from: _$S18StaticSwiftLibrary02MyaC5ClassCMn in libStaticSwiftLibrary.a(StaticSwiftLibrary.o) ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation)Version/Build:Version 10.0 beta 3Notes:We're seeing various "l00x" codes based on which type is used in the static libraryThis occurs with both the old and new build systemThe radar has a full sample project attached. For the purposes of this post, the Swift static library just needs to contain this:public class MyStaticLibraryClass: NSObject {
}$ swift-demangle S18StaticSwiftLibrary02MyaC5ClassCMn
$S18StaticSwiftLibrary02MyaC5ClassCMn ---> nominal type descriptor for StaticSwiftLibrary.MyStaticLibraryClassSo we're guessing the type info for the class is trying to reference some kind of auto-generated incremening L00X symbol representing any ObjC types used, or something?
Post not yet marked as solved
I have a few questions about network streams, particularly NSStreamSocketSecurityLevelKey and what looks like a private property on streams, _kCFStreamPropertySocketPeerNameThe documentation for NSStreamSocketSecurityLevelKey on https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Streams/Articles/NetworkStreams.html states:For SSL security, NSStream defines various security-level properties (for example, NSStreamSocketSecurityLevelSSLv2). You set these properties by sending setProperty:forKey: to the stream object using the key NSStreamSocketSecurityLevelKey, as in this sample message:[inputStream setProperty:NSStreamSocketSecurityLevelTLSv1 forKey:NSStreamSocketSecurityLevelKey];You must set the property before you open the stream. Once it opens, it goes through a handshake protocol to find out what level of SSL security the other side of the connection is using. If the security level is not compatible with the specified property, the stream object generates an error event.We've seen that it is possible to set this property after opening the stream and sending/receiving some initial data, for example doing a handshake with a CONNECT proxy before starting the real TLS session with the final end point.Is this ability to start the TLS negotiation at a later date via a delayed setProperty:forKey:NSStreamSocketSecurityLevelKey fully supported?Also, there is still a problem in the CONNECT proxy case above: the SNI sent to the end-point will be the proxy host not the intended host. Facebook's SocketRocket library uses what looks like a private property to override this:https://github.com/facebook/SocketRocket/blob/41b57bb2fc292a814f758441a05243eb38457027/SocketRocket/Internal/Proxy/SRProxyConnect.m#L99[self.outputStream setProperty:self.url.host forKey:@"_kCFStreamPropertySocketPeerName"];Is this supported? Even though it's kind of private, is this something we could use?PS: Calling Quinn the Eskimo 😉
Post not yet marked as solved
https://github.com/apple/swift/pull/11744 made it a warning to specify `weak` on Swift properties (vars) in protocols. This is present in Xcode 9.3 beta 4. In other words this now generates a warning:protocol MyProtocol {
// 'weak' should not be applied to a property declaration in a protocol and will be disallowed in future versions
weak var delegate: MyDelegate?
}Instead, weak is omitted from the protocol declaration, and Swift implementions of this protocol can redeclare the property as weak themselves:class Impl: MyProtocol {
weak var delegate: MyDelegate?
}However, this is a problem for ObjC implementations since the compiler complains if you redeclare the property and change the strong/weakness:#import <MyFramework-Swift.h>
@interface ImplObjC : NSObject <MyProtocol>
// warning: 'retain (or strong)' attribute on property 'delegate' does not match the property inherited from 'MyProtocol' [-Wproperty-attribute-mismatch]
@property (nonatomic, weak, nullable) id<MyDelegate> delegate;
@endAlternatively in a private category:@interface ImplObjC ()// error: illegal redeclaration of property in class extension 'ImplObjC' (attribute must be 'readwrite', while its primary must be 'readonly')
@property (nonatomic, weak, nullable) id<MyDelegate> delegate;
@endAlternatively synthesizing into a different ivar with __weak:@interface ImplObjC ()
{
// error: existing instance variable 'm_delegate' for strong property 'delegate' may not be __weak
__weak id<MyDelegate> m_delegate;
}
@end
@implementation ImplObjC
@synthesize delegate = m_delegate;
@endThe only alternative that works is to override the setter and getter for the weak property and back it with a strong ivar/property. This is ugly and tedious however.We build with "warnings as errors" for ObjC, so this is a source breaking change for our existing code. It doesn't seem like it would be a good idea to ignore the Wproperty-attribute-mismatch warning. This is a problem for any existing ObjC code that implements Swift protocols like this.Now, there are legitimate design discussions to be had over whether a delegate property belongs on an abstract protocol, but given how common the (weak) delegate pattern is in ObjC I believe there will be a significant amount of this kind of code in the wild (we ran into this at least).Is this a known/expected issue?