Monday, 24 October 2011

Custom HTTP Headers in iPhone UIWebView


If you've done much coding for iPhones, you'll likely have come across the UIWebView class. It is what allows you to use an embeddable web browser component, within your app. It's a useful component that lets you view web pages straight from within your app, using the very capable rendering capabilities granted to the iPhone's full Safari browser.

The problem I faced today, however, was that I needed to customise some of the HTTP headers that are sent to servers, when using the UIWebView component. Turns out that this is possible, although not as straight forward as I first expected. Why might you want to do this? Well you might want to alter the HTTP User Agent string, add some custom headers that your server requires, alter the language supported by the Accept header or you might well just be really curious.

Read the solution after the break.



To achieve this aim, you first need to declare a delegate for the UIWebView. From here, I'll assume you have a UIViewController which contains a UIWebView as a member. Open the .h file for your UIViewController and add the UIWebViewDelegate to your controller's interface signature.

For example:

CustomWebViewController.h
--------------------------------
@interface CustomWebViewController: UIViewController<UIWebViewDelegate>

This is stating that this class can act as a delegate for a UIWebView, which in turn allows for us to provide implementations for certain methods for the UIWebView. The one we're really interested in here is:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

Phew... what a mouthful - objective c methods sure are beautiful!. This method is invoked whenever the web view attempts to load a new request. This turned out to be pretty useful for me, as I wanted to add some custom header to every request that the web view made.

I'll turn to some sample code pretty soon and leave the descriptions. My code is obviously amazingly well documented at all times! Well, code that I'm willing to put on the web is!

A quick summary of the code which follows then. This method is given a NSURLRequest object as a parameter. If this allowed you to manipulate the headers directly, there would be no need for this post. Unfortunately you cannot, and what you really want is a NSMutableURLRequest object. Now in my investigation, I actually found that the NSURLRequest object was a kind of NSMutableURLRequest; that is, I could cast between them. However, changing the given request object will not actually alter the headers. Instead, you check if the request has your custom headers already; if it does, you are done! Let the request go. If it doesn't you then need to create a mutable copy of the NSURLRequest, giving you an NSMutableURLRequest object. You can then alter/add HTTP headers all you please. Cancel the original unmodified request and fire off your new one instead.

It turns out that a very important step to remember (and is easily forgotten) is that because you will be firing off a new request, the shouldStartLoadWithRequest will fire again - that is, it will fire twice for every request. You need to make sure you don't get into an infinite loop, and by looping through all the existing headers to see if your custom one have been set, you can differentiate between the first request (no custom headers initially so they are added) and the second request (custom headers exist so let the request load). If you only have a few custom headers to set, or know that the keys won't change, you can clean up some of the looping logic below, but I don't mind the extra lines of code as it gives good future proofing here in my opinion.

Note, customHeaders variable below is an NSMutableDictionary containing key/value pairs for the HTTP headers and values needing set. I should also note that I'm using Automatic Reference Counting (ARC) available for XCode 4.2 and above. If you are not, then you'll need to go through the code adding in various retain/release/autorelease comments. I have switched to ARC and can't see myself looking back any time soon!


/**
 * Delegate method that is called whenever a link is navigated to in the web view.
 *
 * @param webView The webview that is starting a load
 * @param request The request that the webview is making
 * @param navigationType The type of request it is making
 * @return A boolean as to whether or not it should continue with the load
 */
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
 navigationType:(UIWebViewNavigationType)navigationType
{    
        // if we have no custom headers to set, just let the request load as normal
        if(customHeaders.count == 0)
        {
            return YES;
        }
        
        // use this flag to track if all custom headers have been set on the request object
        BOOL allHeadersProcessed = YES;
        
        // iterate through all specified custom headers
        for(NSString *customKey in [customHeaders allKeys])
        {
            // grab the value associated with the custom header
            NSString *customValue = (NSString *)[customHeaders objectForKey:customKey];
            
            // use this flag to mark if the custom header/value already exist on the request
            BOOL customHeaderProcessed = NO;
            
            // iterate through all the keys in the request
            for(NSString *existingKey in [[request allHTTPHeaderFields] allKeys])
            {                
                // only compare keys which match (ignoring case as the UIWebView may alter case between requests)
                if([customKey caseInsensitiveCompare:existingKey] == NSOrderedSame)
                {
                    // grab the value for the existing key - both key and value must match
                    NSString *existingValue = (NSString *)[request valueForHTTPHeaderField:existingKey];
                
                    // if we have a match here, then key and value match
                    if([customValue isEqualToString:existingValue])
                    {
                        // mark this custom header as being processed
                        customHeaderProcessed = YES;
                        
                        // no point in looking through other existing headers when we've found a match
                        break;
                    }
                }
            }
            
            // if this particular custom header hasn't been processed, then mark that not all headers have been processed
            if(customHeaderProcessed == NO)
            {
                allHeadersProcessed = NO;
                break;
            }
        }
        
        // if all headers exist on the request, no modification is necessary
        if(allHeadersProcessed)
            return YES;
        
        // otherwise, we need to cancel the existing request and create a new (mutable) one         
        NSMutableURLRequest *mutableRequest = [request mutableCopy];
                                
        // for each custom header
        for(NSString *key in [customHeaders allKeys])
        {
            // grab the value needing set
            NSString *value = [customHeaders valueForKey:key];
            
            // set the value to the custom header
            [mutableRequest addValue:value forHTTPHeaderField:key];
                    
        }
                
        // load the new mutable request
        [webView loadRequest:mutableRequest];
        
        // cancel the existing request
        return NO;


}

Et voilà, custom headers in your embedded UIWebView. Not easy, not particularly straight forward, but a viable solution nevertheless.

Enjoy.

2 comments:

  1. This solution does not work clean for pages with frames. It cancels out both the requests mutable and non-mutable when you return NO;

    ReplyDelete
  2. You might be able to store each request as it is created, and store which frame it originated from.

    When deciding whether to cancel a request or not, you could perhaps query the originating frame, and allow the request through or cancel accordingly.

    I don't know if you get access to that info though.

    ReplyDelete