Notes on NSURLSession


I was excited at WWDC 2013 to see iOS 7 and OS X 10.9 introduces NSURLSession as a replacement/upgrade to NSURLConnection. I'm keen on it generally, and thought it worth writing up my notes & thoughts on the new API in case it sparked any interesting discussion or answers.

(Where there are unanswered questions, I've provided feedback through the appropriate mechanism in Apple's documentation, and will endeavour to update this post with future info)

Threading

I'm keen on the use of .delegateQueue to specify how callback blocks and delegate messages should be executed. Without this concept, API designers have roughly two choices:

  • Always execute on the main thread
  • Execute on an unspecified — likely serial — queue; clients are responsible for bouncing over to their desired queue from there, as needed


Always executing on the main thread is often convenient, especially for the first pass at a system. But that could become a bit of a bottleneck for applications handling a lot of requests at once. Besides, for any complicated processing of received data etc. an app likely wants to bounce that work away from the main thread anyway. This is much the same as the latter option; both carry the annoying boilerplate of regularly bouncing over to another thread/queue, and the debugging nightmare when you forget to do so.

It seems cleanest then, to allow clients to specify up-front the queue they'd like to use. It's easy to pass in the main queue if desired, or nil to signal that the session should create its own queue.

Clients appear to be free to modify the delegate queue's properties if they really wish, but I'm struggling to think of any modifications that would actually be useful.

Under iOS 7/OS X 10.9, delegate messages are sent serially, regardless of your queue’s setup. But as of iOS 8/OS X 10.10, if you supply a concurrent queue, delegate messages will take advantage of that. This means messages can be processed in parallel for different tasks. I don’t know if the messaging then becomes serial on a per-task basis, or is concurrent there too; somewhat hope it’s the former for simplicity!

Not documented yet, but apparently it's safe to create tasks from any thread.

Tasks

Slightly to my surprise, NSURLSessionTask is a direct subclass of NSObject. I thought they might have gone with NSOperation or NSURLConnection as a starting point. I can understand wanting to draw a line and declare NSURLConnection finished with though, and NSOperation's semantics don't entirely fit with tasks I'll admit.

Nicely, NSURLSessionTask’s properties are all KVO-compliant. No word yet on which queue/thread those notifications get posted on.

Configuration

NSURLSessionConfiguration is a new class that specifies the broad behaviour of sessions. Many of its properties have corresponding properties in NSURLRequest. This seems a nice compromise; tasks use the settings specified by the session unless you specially craft a request to do otherwise.

An alternative would be for sessions to be created with the concept of a prototype NSURLRequest, which other requests are then based off of. But for properties which aren't applicable to individual requests, this approach doesn't work, so NSURLSessionConfiguration seems to make the most sense.

Configurations are mutable, but Apple doesn't want the headache of mutations happening while a task is running. Rather than have a separate NSMutableURLSessionConfiguration class, they've instead chosen to makes copies of the configuration and keep that copy carefully hidden away so it can't be mutated by clients.

Queuing

Tasks start out life in a suspended state, and clients must first resume them. I wonder if this will become an annoyance to keep typing, and — on those occasions you forget — to debug.

What happens if you try to resume a completed task?

This behaviour should make it quite easy to create a queue for tasks, running a few at a time, and leaving the others suspended. I wonder if we'll see NSURLSessionTaskQueue or some such in iOS 8.

HTTPMaximumConnectionsPerHost

There's also NSURLSessionConfiguration.HTTPMaximumConnectionsPerHost. It seems this can be used as a primitive queuing mechanism, by starting up all tasks immediately and letting the session throttle them. Apparently we need not worry about weird timeouts or other failures during this, since request timeouts don’t start being applied until the underlying connection is actually attempted.

Apparently this is applied on a per-session basis. For example, in a web browser, I imagine each window/tab would get its own NSURLSession, with a reasonable limit on the number of connections per host. But then the user is free to open as many as they like, upping the overall number of connections.

Oh, and there's no documentation of what the default value is. (For added fun, it's an NSInteger; what happens if you try to specify a negative number or zero?!)

Completion (updated since this was first posted)

For completion handlers, Apple sticks firm here with a single callback block to handle both success and failure, something I totally agree with. I'm interested to see this actually extends to the delegate methods now; there's a single completion message, rather than separate ones for "finish" and "fail". This will probably serve to give me slightly smaller code.

Somewhat related: if you try to create an NSURLConnection using an unsupported URL (e.g. rdar://12345) it will fail and return nil. Granted, this should be pretty rare, but when it does happen your code will likely fail in interesting ways (unless you’re rigorous enough to always assert your connections are non-nil for example). NSURLSessionTask consolidates the code paths by always waiting until you resume the task and then failing with NSURLErrorUnsupportedURL (or NSURLErrorBadURL if you’re crazy enough to pass in nil).

Cancellation (new since this was first posted)

When cancelled, NSURLConnection stops the actual connection, and ceases sending you any more delegate messages. In practice this proves a little annoying: in addition to handling success and failure, you have to write code to account for the cancellation path too.

With its single completion block/delegate method, NSURLSession simplifies this too. Cancelled tasks still call it, with a NSURLErrorCancelled error. Most of the time you probably just need to test for this before presenting any errors in your UI.

Another interesting tweak is the inclusion of NSURLSessionTaskStateCanceling. As I understand it, once cancelled, tasks may still report back any already enqueued messages, before reaching the final completion block/message. It's unclear to me if tasks then transition to NSURLSessionTaskStateCompleted, or remain in the "canceling" state. (Yes, I'm being lazy here; will update when I know!)

Again undocumented, but apparently it’s OK to call -cancel on any thread/queue.

NSCopying

Interestingly, both NSURLSession and NSURLSessionTask conform to NSCopying, and are documented to do so by returning "the same object back". This seems a departure from the rest of Cocoa. I figure it's to make using tasks and sessions as keys in a dictionary nice and easy.

(Brent Simmons recently wrote about storing custom data for a task and he thinks associated objects is the way to go at present. I think because of the above, maintaining an NSDictionary makes slightly more sense, and ultimately both have the same performance characteristics.)

Authentication (updated since this was first posted)

NSURLSession has two separate delegate methods for handling authentication. One is task-specific, and the other only handles broader "session-level" challenges (e.g. evaluating server trust).

I don't see the benefit in this (yet), but expect Apple have good reasons. For convenience, if clients only implement the task-specific delegate method, all challenges will be routed there. This seems nice at first glance, but now I'm wondering if it could be awkward in practice for clients which are only interested in task-specific auth (e.g. username & password), and want the system to handle other challenges (e.g. server trust) for them.

NSURLAuthenticationChallengeSender has been more-or-less superseded in the new API by a block-based callback instead. I'm not sure there's any benefit to this either really (beyond being easier for Apple to implement), but will give them the benefit of the doubt again!

The completion handler block takes an enum as its first argument, declaring how you’d like the challenge to be handled. This matches up quite neatly to NSURLAuthenticationChallengeSender’s methods, minus -continueWithoutCredentialForAuthenticationChallenge:. Pass NSURLSessionAuthChallengeUseCredential and nil to get its behaviour if needed.

The mysterious -…didCancelAuthenticationChallenge: message is gone too.

Custom Protocols

Custom NSURLProtocol subclasses are now registered on a per-session basis (as part of the configuration), better keeping them contained to specific sub-systems. This provides a little more incentive to expose such classes to clients of a framework to register at their discretion, rather than quietly automatically registering for all requests.

Some annoyances still stand. In particular, custom protocols still have no means to report back upload progress, or request a fresh stream (rdar://13170210). The new session API also introduces at least one new annoyance: protocols issuing an authentication challenge have no way to indicate whether it's session-level, or task-specific (rdar://15276192).

Uploads (new since this was first posted)

Due to the nature of HTTP, all requests can include a payload of body data. But NSURLSession extends this with a dedicated NSURLSessionUploadTask class, and creation methods that make explicit the source to be uploaded (file, data or stream).

For the common cases of files and raw data, this nicely takes care of handling scenarios where the body data has to be sent more than once (e.g. authentication challenges or redirects) with no extra effort on your part. Before you had to implement -connection:needNewBodyStream: (which has a lovely primal ring to it, don’t you think?) to handle uploading from a file.

When uploading from a stream, the equivalent method -URLSession:task:needNewBodyStream: needs to be implemented. It’s now asynchronous, with a completion handler for your delegate to report back the new stream at its leisure (I suppose the aforementioned lack of API for custom protocols to request fresh streams made this change a lot easier for Apple). The delegate method is called to retrieve a stream for the initial upload attempt, not just followups, making it a lot clearer for clients what they need to do, and easier to test.

One thing I’ve never really understood is how to handle a failure when creating the stream. I assume, the delegate reports back nil for the stream, but there’s no provision to tell the session/task/connection why that’s nil. Presumably the delegate is responsible for handling & presenting that error information as it sees fit.

A common use many Cocoa developers run into is sending a POST request with form data. Sadly NSURLSession introduces no new API for easing the construction of such forms. For small amounts of information, it’s fairly easy to construct a single blob of data and attach it as the request’s -HTTPBody. But for larger requests, particularly on iOS, this can exhaust memory. Under NSURLConnection, this was a pain to handle; attempting to subclass NSInputStream would fail when the URL loading system tried to schedule the stream using non-public API. Instead, the best approach it seems was to write the form data into an NSOutputStream, either to a file, or paired up with the requests -HTTPBodyStream. As far as I am aware, NSURLSession doesn’t change this state of affairs, although I haven’t tried it yet, admittedly.

HTTP

Usage of NSURLConnection often required casting NSURLResponse to NSHTTPURLResponse it seemed. The new API seems slightly more HTTP-focused, promising us NSHTTPURLResponse instances directly in a few cases.

There’s still no automatic treatment of HTTP codes above 300 as errors, which a lot of people have complained about over the years. Perhaps it's best for us as developers to have to think about this, since there's also the question of receiving other, unexpected status codes below 300. And of course captive wi-fi networks often mean our apps receive 200 status codes, but with completely unsuitable body data.

The combination of compressed HTTP responses, and progress feedback, still appears to be impossible through NSURLSession (rdar://13170195). To rectify, we'd need a mechanism which reports the length of data actually received in addition to the decompressed data itself. I notice NSURLSessionTask has methods for reporting expected and received bytes; I wonder if that differs to NSURLResponse, since the expected size is redundant otherwise.

Redirects and Request Customisation (new since this was first posted)

NSURLConnection offers a delegate method: -connection:willSendRequest:redirectResponse:. It does double-duty, acting as both a means to handle redirects, and to customise requests before they’re sent.

It seems NSURLSession changes this with a more specialised -URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: method. As with a lot of the new APIs, it’s asynchronous, and explicitly passes an HTTP response object. If you’re writing a custom protocol which supports redirects and doesn’t use NSHTTPURLResponse to do so, I have no idea how NSURLSession will react.

This removes the ability to customise requests before they’re sent. Instead, the customisation in many cases has to be performed explicitly up-front by passing a fully formed request to the session. For some things, like HTTP headers, it’s also possible to apply changes as part of the session configuration too.

Big Downloads

For a lot of cases, Cocoa apps are downloading data form a server to parse in-memory. It seems to me that if anything causes the server to start returning an unexpectedly large payload (e.g. 100 MB+), most apps aren't setup to deal with this, and will crash. Perhaps it would be nice if Apple gave us an easy mechanism to specify the maximum expected file size for a request or session, and give up mid-download should it be reached.

Reachability

A great number of developers use the Reachability APIs in their app in one form or another (a lot do so inappropriately, it must be said). NSURLSession appears to offer no new integration here. e.g. a means to automatically retry a task when the network changes could be nice. Maybe another one we might see in iOS 8.

© Mike Abdullah 2007-2015