blog

Image of Elephants by Mylon Ollila on Unsplash

Skipping the WebKit cache

by

Caches in WebKit are tenacious. That’s problematic in an app that uses an embedded WebKit view when you’re trying to load your latest JavaScript or CSS changes without restarting the application. Chrome and Safari both provide developer tools for clearing and disabling caches, so I assumed it would be relatively straight-forward to implement a similar option in a custom app.

It was not. There are various methods that should disable or empty the caches, but at least in a Cocoa app with an embedded WebView running in OS X 10.8.5, most of them don’t work.

WebCache to the rescue

A post on this topic pointed me to the only solution I’ve found that reliably forces all requests to exit WebKit and reach the server. It uses the WebCache interface, which Apple does not expose through its public APIs.

// Declare the private WebCache interface so
// that it can be cleared / disabled.
@interface WebCache : NSObject
+ (void)empty;
+ (void)setDisabled:(BOOL)arg1;
@end
...
// Disable or enable the cache
[WebCache setDisabled:disabled];

Two important notes:

  1. Because this interface is not exposed by Cocoa’s APIs, using it might result in an app rejection if you submit this code to the Apple’s App Store. It may not constitute a “private” API to Apple since it’s part of the open source WebKit code, but it might be safest to remove it from production builds.

  2. In my test, re-enabling the WebCache had no effect: after I disabled and then re-enabled the cache, all requests continued to be sent to the server.

Failed solutions

Here are the various solutions I attempted prior to discovering the WebCache interface. All failed, either because they don’t clear the appropriate caches (JavaScript and CSS resources appear to be cached separately from HTML pages) or because of bugs in WebKit or the related Cocoa APIs.

Set the cachePolicy for NSURLRequests to one that ignores cached data

# Configure an object as the WebResourceLoadDelegate for the WebView.
[self.webView setDownloadDelegate:self];
...
# Intercept all the URL requests for the WebView and
# set the cache policy to ignore the cache
-(NSURLRequest*)webView:(WebView*)webView resource:(id)identifier
        willSendRequest:(NSURLRequest*)request
       redirectResponse:(NSURLResponse*)redirectResponse
         fromDataSource:(WebDataSource*)dataSource
{
   return [NSURLRequest requestWithURL:request.URL
                           cachePolicy:NSURLRequestReloadIgnoringCacheData
                       timeoutInterval:request.timeoutInterval];
}

This had no discernible effect. It was broken at least once before, and perhaps it is again.

Add a cache busting query param

# Configure an object as the WebResourceLoadDelegate for the WebView.
[self.webView setDownloadDelegate:self];
...# Intercept all the URL requests for the WebView and
# append a unique query param
-(NSURLRequest*)webView:(WebView*)webView resource:(id)identifier
        willSendRequest:(NSURLRequest*)request
       redirectResponse:(NSURLResponse*)redirectResponse
         fromDataSource:(WebDataSource*)dataSource
{
   NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
   NSString* cacheBusterParam = [NSString stringWithFormat:@"cbts=%f", ts];
   NSURL* newURL = request.URL;
   NSString* URLString = [[NSString alloc] initWithFormat:@"%@%@%@",
                          newURL.absoluteString,
                          newURL.query ? @"&" : @"?",
                          cacheBusterParam];
   newURL = [NSURL URLWithString:URLString];
   return [NSURLRequest requestWithURL:newURL
                           cachePolicy:request.cachePolicy
                           timeoutInterval:request.timeoutInterval];
}

Surprisingly, WebKit doesn’t actually respect these URL changes if it has already cached the response for the original URL. It appears to ignore the URL changes and return the cached response for the original URL.

Create a new shared NSURLCache with no memory or disk capacity

NSURLCache* newCache = [[NSURLCache alloc] initWithMemoryCapacity:0
                                                     diskCapacity:0
                                                     diskPath:nil];
[NSURLCache setSharedURLCache:newCache];

Remove all cached responses from the shared NSURLCache

[[NSURLCache sharedURLCache] removeAllCachedResponses];

Implement an NSURLProtocol that disables the cache

I didn’t save the code from this failed attempt, but check out this NSHipster post elucidating NSURLProtocol.  NSURLProtocol allows you to squeeze a powerful object between URL requests and their responses.  Theoretically it should allow for overriding the caches.
It didn’t.

+ more