4

Would a file with a response header Cache-Control:Private be prevented from being cached in a NSURLCache? Either a shared cache (as in setSharedCache and NSURLCache.sharedCache() ) or a custom one?

To expand, I have UIWebView that I need to access when offline. The source of this WebView has multiple external CSS and JS files associated with it. I can cache most of the site (the CSS, etc looks in place), however it seems to not be caching a specific JavaScript file that provides important information for the site. A difference that I noted between the file that won't cache and the rest is that it's Cache-Control is set to private (the others are public). However, from what I have read, setting the cache control to private is to prevent caching on proxies. Would it affect caching on iOS?

Setting up the cache

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    let URLCache: NSURLCache = NSURLCache(memoryCapacity: 10 * 1024 * 1024,
                                            diskCapacity: 50 * 1024 * 1024,
                                                diskPath: nil)

    NSURLCache.setSharedURLCache(URLCache)

    println("Disk cache usage: \(NSURLCache.sharedURLCache().currentDiskUsage)")

    // http://stackoverflow.com/questions/21957378/how-to-cache-using-nsurlsession-and-nsurlcache-not-working
    sleep(1)

    return true
}

Using the cache

func getWebPage(onCompletion: (NSString, NSURL) -> Void) {

    let url = getApplicationSelectorURL()

    let request = NSURLRequest(URL: url, cachePolicy: .ReturnCacheDataElseLoad, timeoutInterval: 10.0)

    let queue = NSOperationQueue()

    NSURLConnection.sendAsynchronousRequest(request, queue: queue, completionHandler: { response, data, error in
        println("Web page task completed")

        var cachedResponse: NSCachedURLResponse

        if (error != nil) {
            println("NSURLConnection error: \(error.localizedDescription)")
            if let cachedResponse = NSURLCache.sharedURLCache().cachedResponseForRequest(request) {
                if let htmlString = NSString(data: cachedResponse.data, encoding: NSUTF8StringEncoding) {
                    onCompletion(htmlString, url)
                } else {
                    println("htmlString nil")
                }
            } else {
                println("cacheResponse nil")
            }
        } else {
            cachedResponse = NSCachedURLResponse(response: response, data: data, userInfo: nil, storagePolicy: .Allowed)
            NSURLCache.sharedURLCache().storeCachedResponse(cachedResponse, forRequest: request)
            if let htmlString = NSString(data: data, encoding: NSUTF8StringEncoding) {
                onCompletion(htmlString, url)
            } else {
                println("htmlString nil")
            }
        }
    })
}

Populating the UIWebView

APICommunicator.sharedInstance.getWebPage({ htmlString, url in

    dispatch_async(dispatch_get_main_queue(),{
        self.webView.loadHTMLString(htmlString, baseURL: url)
    })

})
Ashish Kakkad
  • 23,586
  • 12
  • 103
  • 136
cschandler
  • 544
  • 5
  • 15

2 Answers2

1

I ended up creating a method similar to the NSURLConnectionDelegate method willCacheResponse, and replacing the Cache-Control:private header.

willCacheResponse method

func willCacheResponse(cachedResponse: NSCachedURLResponse) -> NSCachedURLResponse?
{        
    let response = cachedResponse.response

    let HTTPresponse: NSHTTPURLResponse = response as NSHTTPURLResponse

    let headers: NSDictionary = HTTPresponse.allHeaderFields

    var modifiedHeaders: NSMutableDictionary = headers.mutableCopy() as NSMutableDictionary

    modifiedHeaders["Cache-Control"] = "max-age=604800"

    let modifiedResponse: NSHTTPURLResponse = NSHTTPURLResponse(
            URL: HTTPresponse.URL!,
            statusCode: HTTPresponse.statusCode,
            HTTPVersion: "HTTP/1.1",
            headerFields: modifiedHeaders)!

    let modifiedCachedResponse = NSCachedURLResponse(
            response: modifiedResponse,
            data: cachedResponse.data,
            userInfo: cachedResponse.userInfo,
            storagePolicy: cachedResponse.storagePolicy)

    return modifiedCachedResponse
}

calling method

if let cachedResponse = self.willCacheResponse(
        NSCachedURLResponse(response: response,
                                data: data,
                            userInfo: nil,
                       storagePolicy: .Allowed)) {

    NSURLCache.sharedURLCache().storeCachedResponse(cachedResponse, forRequest: request)
}

And now it displays correctly when offline. What a journey.

cschandler
  • 544
  • 5
  • 15
0

Yes, responses with the private cache control policy are not being cached by the NSURLCache. The RFC #2616 says

private: Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache. This allows an origin server to state that the specified parts of the response are intended for only one user and are not a valid response for requests by other users. A private (non-shared) cache MAY cache the response.

Well, NSURLCache uses sharedCache which you even set up in the code you posted. I guess it explains pretty much everything.

Solution is either to change the server behaviour, or to override some methods of the NSURLCache class. (You can e.g. rewrite the header client-side, but this should be quite an awful hack.)

curlybracket
  • 413
  • 2
  • 9
  • Hmmm.. I tried with a private (non-shared) cache, but with the same results. I'm currently diving into awful hack territory, as outlined in the [Forcing Response Caching](http://www.hpique.com/2014/03/how-to-cache-server-responses-in-ios-apps/) section of this blog post. Unfortunately, I do not have the option of changing the server behavior. – cschandler Mar 08 '15 at 05:35
  • @cschandler Link is broken ...and NSFW – Alexandre Cassagne Jan 21 '20 at 21:31