Intercepting Requests with NSURLProtocol

 ·  iOS, Development, Libraries, Tutorials  ·  Tagged URLMock, URL Loading, Networking and NSURLProtocol

We’ve been discussing URLMock a lot recently. We introduced the library and provided some use cases for it. Later, we discussed how we used NSProxy to build the message counting system URLMock uses for testing. That’s all great, but how does URLMock actually work?

To step back for a moment, let’s review what URLMock is actually doing. From our earlier post:

“URLMock is an open-source Cocoa framework for mocking and stubbing URL requests and responses … It works seamlessly with apps that use the NSURL loading system, e.g., NSURLConnection or AFNetworking. You don’t even have to change your client code.”

There are a few things to take away from this:

  1. URLMock provides mocking or stubbing for URL requests and responses.
  2. You don’t have to change your client code if it uses NSURLConnection or AFNetworking.
  3. While we don't mention this explicitly, URLMock doesn't use any private APIs to intercept your client code's requests and respond to them.

UMKMockURLProtocol is the the primary interface into URLMock. Once you enable this protocol it intercepts your app’s requests and responds to them as you specify. So, how does it work? How can we intercept requests sent by your application while being a good citizen and avoiding private APIs?

Introducing NSURLProtocol

Foundation’s URL loading system provides many handlers for responding to URL-based requests. Out of the box, it supports for several protocols, including http:, https:, ftp:, file:, data:. What if you had your own protocol that’s not supported? Do you have to go build the whole URL stack from scratch?

Thankfully, the answer is no. NSURLProtocol is your hook into Foundation’s URL Loading system. You create a subclass of NSURLProtocol that implements your protocol and register it with the URL loading system. When a request is sent, all registered protocols—from the most to least recently registered—are asked if they can handle the request. The first protocol to respond positively is chosen to handle the request. You don't even need to create an instance of your protocol, the system does this for you.

Once your protocol is chosen, it is sent the ‑startLoading message where you can decide how to respond to the request. At this point, your responsibilities are to provide the client (a property of NSURLProtocol that conforms to the NSURLProtocolClient protocol) with information about the loading process. If you've ever implemented the delegate callbacks for NSURLConnection, these methods should look a bit familiar to you.

The best part here is that your protocols take precedence over Apple’s, so you can change basic URL loading behavior for existing protocols like http. This is exactly how URLMock works. When you enable UMKMockURLProtocol, you are registering it as a handler in the URL loading system. When asked if it can handle a request, it checks its list of registered mock requests and returns YES if it finds a match.

Let’s see how this works by implementing our own NSURLProtocol.

Introducing TWTHasselhoffImageProtocol

Suppose you are working on displaying remote images in your app and are about to board an airplane. Sure, you could temporarily change all your images to be local, but then your client code would be littered with all these changes that you’ll need to delete later. Let’s make a protocol that responds with an image from the bundle whenever an image is requested. For maximum awesomeness, let’s make that image always be a photo of David Hasselhoff.

First we have our client code. This is the code that will request the photo we'd like to display.

NSString *kittenImageString = @"http://cutekittens.com/kittens_cute_kitten.jpg";
NSURL *url = [NSURL URLWithString:kittenImageString];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request addValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];

[NSURLConnection sendAsynchronousRequest:request queue:self.imageQueue completionHandler:
^(NSURLResponse *response, NSData *data, NSError *connectionError) {

    // Create the image directly from the data response.
    UIImage *kittenImage = [UIImage imageWithData:data scale:0.0];

    // Update the image view with the image    
    [[NSOperationQueue mainQueue] addOperation:[NSBlockOperation blockOperationWithBlock:^{
        self.imageView.image = kittenImage;
    }]];
}];

This works as expected. We run the application and see our cute kitten.

Now we start to have some fun. We’ll create TWTHasselhoffImageProtocol, which intercepts image requests and responds with some Hasselhoff. You can download this class and follow along.

The first thing we need to do is tell the URL loading system if we can handle this request. We do this using +canInitWithRequest:. We’ll return YES if the request is for a PNG or JPEG.

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    NSSet *validContentTypes = [NSSet setWithArray:@[ @"image/png",
                                                      @"image/jpg",
                                                      @"image/jpeg" ]];

    return [validContentTypes containsObject:request.allHTTPHeaderFields[@"Content-Type"]];
}

Next we'll implement ‑startLoading and ‑stopLoading and simply return our Hasselhoff photo.

 (void)startLoading
{
    id <NSURLProtocolClient> client = self.client;
    NSURLRequest *request = self.request;

    NSDictionary *headers = @{ @"Content-Type": @"image/jpeg" };
    NSData *imageData = UIImageJPEGRepresentation([UIImage imageNamed:@"David_Hasselhoff.jpeg"], 1.0);

    NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL
                                                              statusCode:200
                                                             HTTPVersion:@"HTTP/1.1"
                                                            headerFields:headers];

    [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [client URLProtocol:self didLoadData:imageData];
    [client URLProtocolDidFinishLoading:self];
}

 (void)stopLoading
{
    // We send all the data at once, so there is nothing to do here.
}

The only remaining piece of the puzzle here is the registration step. We’ll simply register it at the app’s launch.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [NSURLProtocol registerClass:[TWTHasselhoffImageProtocol class]];
    return YES;
}

Now when we run the application, instead of seeing a cute kitten we should see Hasselhoff. The idea that you don’t have to change your client code should be clear. We are still requesting the cute kitten image, but our protocol knows better. It knows what we really wanted was the Hoff.

Summary

Foundation’s URL loading system is a powerful abstraction around network requests. It allows you to easily add your own handlers to provide support for new protocols or change existing behavior. This can help a lot when you are missing resources and need to stay productive.

URLMock is a great example of the things you can do by exploiting NSURLProtocol. It provides a lightweight interface to intercept requests and respond however you see fit. Take some time to look through UMKMockURLProtocol to explore a more detailed example of an NSURLProtocol subclass.

If you have any questions about this article or NSURLProtocol, get in touch with me on Twitter at @jnjosh.