run2thesun
run2thesun

Reputation: 398

UIWebView not loading contents of redirected page when using custom NSURLProtocol

I have a UIWebView and a custom NSURLProtocol class registered to proxy HTTP requests. I have a problem loading github.com on it. If I browse to https://github.com then it loads the page and its contents just fine. However, if I browse to http://github.com, it loads the HTML correctly but it doesn't load the images or CSS. Here's how it looks like when I load the https version:

HTTPS GitHub

Here's how it looks like when I load the http version:

HTTP GitHub

Here's the code for the view controller that I used to reproduce this problem:

@interface ViewController ()
{
    UIWebView *aWebView;
}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [NSURLProtocol registerClass:[WebViewProxyURLProtocol class]];

    aWebView = [[UIWebView alloc] initWithFrame:self.view.frame];
    aWebView.delegate = self;
    [self.view addSubview:aWebView];

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://github.com"]];
    [aWebView loadRequest:request];
}

@end

Here's the NSURLProtocol implementation:

@interface WebViewProxyURLProtocol : NSURLProtocol <NSURLConnectionDataDelegate>

@end

@implementation WebViewProxyURLProtocol {
    NSMutableURLRequest* correctedRequest;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    return ([[request allHTTPHeaderFields] objectForKey:@"X-WebViewProxy"] == nil);
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return NO;
}

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
    if (self = [super initWithRequest:request cachedResponse:cachedResponse client:client]) {
        // Add header to prevent loop in proxy
        correctedRequest = request.mutableCopy;
        [correctedRequest addValue:@"True" forHTTPHeaderField:@"X-WebViewProxy"];
    }
    return self;
}

- (void)startLoading {
    [NSURLConnection connectionWithRequest:correctedRequest delegate:self];
}

- (void)stopLoading {
    correctedRequest = nil;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

- (NSURLRequest *)connection:(NSURLConnection *)connection
             willSendRequest:(NSURLRequest *)request
            redirectResponse:(NSURLResponse *)redirectResponse
{
    return request;
}

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    return nil;
}

@end

If I disable the custom NSURLProtocol class, it works fine. I used Charles to inspect the HTTP requests and responses and they look identical with and without NSURLProtocol.

So the question is: why does UIWebView not request the contents of the web page when using NSURLConnection to get the page data?

Upvotes: 3

Views: 3967

Answers (3)

Matt Birman
Matt Birman

Reputation: 11

For those looking for a Swift solution, I have taken what @micmdk did and adapted it.

In my case, I intercept requests with a URLProtocol class. In the override func startLoading() method I set a property on the request:

override func startLoading() {
    ...
    guard let mutableRequest = request as? NSMutableURLRequest else {
        exit(0)
    }

    URLProtocol.setProperty("true", forKey:"requestHasBeenHandled", in: mutableRequest)

    return mutableRequest as URLRequest
}

This prevents redirects from being handled. So I remove the property, like so:

func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
    guard let redirect = request as? NSMutableURLRequest else {
        exit(0)
    }

    URLProtocol.removeProperty(forKey: "requestHasBeenHandled", in: redirect)

    self.client?.urlProtocol(self, wasRedirectedTo: redirect as URLRequest, redirectResponse: response)
}

Have confirmed that it works. Images and assets load correctly.

Upvotes: 1

micmdk
micmdk

Reputation: 951

We had the same work - the redirect worked but the content wasn't loaded.

Following the Apple doc they mention that redirects should be handled using the connection:willSendRequest:redirectResponse delegate method in the NSURLProtocol subclass.

Our solution did end up looking like this:

- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
  if (response) {
    NSMutableURLRequest *redirect = [request mutableCopy];
    [NSURLProtocol removePropertyForKey:kFlagRequestHandled inRequest:redirect];
    [RequestHelper addWebViewHeadersToRequest:redirect];

    // THE IMPORTANT PART
    [self.client URLProtocol:self wasRedirectedToRequest:redirect redirectResponse:response];

    return redirect;
  }

return request;

}

Upvotes: 5

run2thesun
run2thesun

Reputation: 398

I figured this out eventually. When I get a redirect response (HTTP 3xx) in connection:didReceiveResponse: I had to call [self.client URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response].

Upvotes: 4

Related Questions