Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS 7 UIWebView 304 cache bug, blank pages

I have a problem I have discovered in my app that has a UIWebView. iOS 7 caches a blank body 304 response, resulting in blank pages being shown when the user refreshes the UIWebView. This is not good user expierience and I'm trying to figure out how to solve this on the iOS side, as I do not have control over how Amazon S3 responds to headers (that's who I use for my resource hosting).

More details of this bug were found by these people: http://tech.vg.no/2013/10/02/ios7-bug-shows-white-page-when-getting-304-not-modified-from-server/

I'd appreciate any help offered to how I can solve this on the app side and not the server side.

Thank you.

Update: fixed this bug using the bounty's suggestion as a guideline:

@property (nonatomic, strong) NSString *lastURL;

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

    if ([self.webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"].length < 1)
    {
        NSLog(@"Reconstructing request...");
        NSString *uniqueURL = [NSString stringWithFormat:@"%@?t=%@", self.lastURL, [[NSProcessInfo processInfo] globallyUniqueString]];
        [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];
    }
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    self.lastURL = [request.URL absoluteString];
    return YES;
}
like image 763
klcjr89 Avatar asked Oct 21 '22 14:10

klcjr89


2 Answers

You can implement a NSURLProtocol, and then in +canonicalRequestForRequest: modify the request to override the cache policy.

This will work for all requests made, including for static resources in the web view which are not normally consulted with the public API delegate.

This is very powerful, and yet, rather easy to implement.

Here is more information: http://nshipster.com/nsurlprotocol/

Reference: https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Classes/NSURLProtocol_Class/Reference/Reference.html


Here is an example:

@interface NoCacheProtocol : NSURLProtocol

@end

@implementation NoCacheProtocol

+ (void)load
{
    [NSURLProtocol registerClass:[NoCacheProtocol class]];
}

+ (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
{
    if ([NSURLProtocol propertyForKey:@“ProtocolRequest” inRequest:theRequest] == nil) {
        return YES;
    }
    return NO;
}

+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
    NSMutableURLRequest* request = [theRequest mutableCopy];
    [request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData];
    //Prevent infinite recursion:
    [NSURLProtocol setProperty:@YES forKey:@"ProtocolRequest" inRequest:request];

    return request;
}

- (void)startLoading
{
    //This is an example and very simple load..

    [NSURLConnection sendAsynchronousRequest:self.request queue:[NSOperationQueue currentQueue] completionHandler:^ (NSURLResponse* response, NSData* data, NSError* error) {
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
    }];
}

- (void)stopLoading
{
    NSLog(@"something went wrong!");
}

@end
like image 125
Léo Natan Avatar answered Oct 23 '22 03:10

Léo Natan


As the other questions are to use the NSURLConnection every time, which seems like a bit of an overhead: Why don't you execute a small javascript after the page was loaded (complete or incomplete) that can tell you if the page is actually showing? Query for a tag that should be there (say your content div) and give a true/false back using the

[UIWebView stringByEvaluatingJavaScriptFromString:@"document.getElementById('adcHeader')!=null"]

And then, should that return false, you can reload the URL manually using the cache-breaker technique you described yourself:

NSString *uniqueURL = [NSString stringWithFormat:@"%@?t=%d", self.url, [[NSDate date] timeIntervalSince1970]]; 
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:uniqueURL] cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:5.0]];

[edit]

based on the discussion in the comments and some of the other answers, I think you might have the best solution manually changing the NSURLCache.

From what I gathered, you're mainly trying to solve a reload/reshow scenario. In that case, query the NSURLCache if a correct response is there, and if not delete the storedvalue before reloading the UIWebView.

[edit 2]

based on your new results, try to delete the NSURLCache when it is corrupted:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache]cachedResponseForRequest:request];

  if (cachedResponse != nil && [[cachedResponse data] length] > 0)
  {
      NSLog(@"%@",cachedResponse.response);
  } else {
    [[NSURLCache sharedURLCache] removeCachedResponseForRequest:request];
  }

  return YES;
}

We might have to refine the check if the cache is invalid again, but in theory this should do the trick!

like image 28
Blitz Avatar answered Oct 23 '22 03:10

Blitz