Overriding HTTP user agent for calls to -initWithContentsOfURL:

Perhaps you need to override the HTTP user agent whenever you call -initWithContentsOfURL: from classes such as NSString, NSDictionary or NSArray, or one of this method’s convenience wrappers such as +stringWithContentsOfURL:, +dictionaryWithContentsOfURL: or +arrayWithContentsOfURL:. So let’s consider how this can be accomplished under iOS.

From what I can see, there is no easy and “clean” way apart from adding a category on the classes where you need to support this and writing your own implementation of -initWithContentsOfURL: and convenience functions (with a slightly different name, of course). These implementations would use NSURLConnection‘s +sendSynchronousRequest:returningResponse:error:. Of course, as with -initWithContentsOfURL: you’d use this replacement method in a background thread to maintain UI responsiveness.

You’d have to write a reimplementation of -initWithContentsOfURL: because the first place you can change this is NSURLRequest, or more specifically, its mutable variant NSMutableURLRequest, using the -setValue:forHTTPHeaderField:. But, if you have tons of code, you probably can’t easily change it to use the new method.

So I dug in and, with a few smart tricks (such as feeding a broken non-NSURL as a NSURL to figure out which methods get called, then implementing them as necessary), I figured out which of several ways for fetching web content is actually used in NSString‘s implementation of -initWithContentsOfURL:. These could have been NSURLConnection or some low level messing with CFNetwork.

It turned out not to matter since NSURLRequest is generated out of the NSURL passed to the method. Customizing the user agent turned out to be just a matter of taking all NSURLRequests, forcing them to become mutable copies in form of instances of NSMutableURLRequest during the initializer and setting the user agent at that time. Specific initializer appearing in iOS implementation used in iOS 5 Simulator that ships with Xcode 4.2.1 appears to be -initWithURL:cachePolicy:timeoutInterval:.

It’s an enormous hack, but I decided to simply swizzle this method out. Swizzling NSURLConnection‘s class method +sendSynchronousRequest:returningResponse:error: did not appear to work – the original method still got called despite my best efforts to figure out what went wrong with swizzling, so I gave up on it. If you can see a mistake in my class swizzling code, please tell me about it in the comments section below.

I definitely have no idea whether or not your app will be rejected for this, but from what I know, method swizzling is not illegal.

//  NSURLRequest+UserAgentFix.m

#define YOUR_USER_AGENT @"Your User Agent"
#import "NSURLRequest+UserAgentFix.h"
#import "NSObject+ISSwizzling.h"
@implementation NSURLRequest (UserAgentFix)
+(void)load
{
    [self swizzleMethod:@selector(initWithURL:cachePolicy:timeoutInterval:)
             withMethod:@selector(initWithURL2:cachePolicy:timeoutInterval:)];
}
-(id)initWithURL2:(NSURL *)URL cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval
{
    self = [self initWithURL2:URL cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    
    if(!self)
        return nil;
    
    if([self class] == [NSURLRequest class])
        self = [self mutableCopy];
    
    if([self class] == [NSMutableURLRequest class])
    {
        NSMutableURLRequest * req = self;
        [req setValue:YOUR_USER_AGENT forHTTPHeaderField:@"User-Agent"];
    }
    
    return self;
}
@end

// NSURLRequest+UserAgentFix.h

#import <Foundation/Foundation.h>

@interface NSURLRequest (UserAgentFix)

@end
// NSObject+ISSwizzling.h

#import <Foundation/Foundation.h>

@interface NSObject (ISSwizzling)
+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector;
+ (BOOL)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector;

@end
// NSObject+ISSwizzling.m

#import <objc/runtime.h>
#import "NSObject+ISSwizzling.h"

@implementation NSObject (ISSwizzling)
+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Method origMethod = class_getInstanceMethod(self, origSelector);
    Method newMethod = class_getInstanceMethod(self, newSelector);
    
    if (origMethod && newMethod) {
        if (class_addMethod(self, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(self, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
+ (BOOL)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Method origMethod = class_getClassMethod(self, origSelector);
    Method newMethod = class_getClassMethod(self, newSelector);
    
    Class class = object_getClass((id)self);

    if (origMethod && newMethod) {
        if (class_addMethod(class, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(class, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}

@end

Tested on iOS 5 Simulator with NSString‘s +stringWithContentsOfURL:.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 

What is 11 + 4 ?
Please leave these two fields as-is:
IMPORTANT! To be able to proceed, you need to solve the following simple math (so we know that you are a human) :-)