Search code examples
swiftnsurlsessionconfigurationnsurlrequestcachepolicy

NSURLRequestCachePolicy.UserProtocolCachPolicy requirements


I am using the following code for caching, the response received form the server has the following headers. Is there any header that needs to be set from the request side, for the caching to work for 10 seconds of age.

Connection Received Resopnse Headers= [Date: Sat, 12 Sep 2015 22:51:16 GMT, Transfer-Encoding: Identity, Server: Apache-Coyote/1.1, Content-Type: application/json;charset=UTF-8, Expires: Sat, 12 Sep 2015 22:51:26 GMT, Cache-Control: max-age=10, must-revalidate]

The mighty code which is not caching.

import UIKit

class HTTPJSONDonwload: NSObject , NSURLConnectionDataDelegate , NSURLConnectionDelegate {
    static let httpjsonDownloader:HTTPJSONDonwload = HTTPJSONDonwload()

    func startDownload(){
        let serverRequest = getServerURL()

        NSURLConnection(request: serverRequest, delegate: self, startImmediately: true)
    }

    func getServerURL() -> NSMutableURLRequest{

        let request:NSMutableURLRequest = NSMutableURLRequest(URL:NSURL(string:"http://citiesfav-jcitiesj.rhcloud.com/Cache/getAllCities")! )
        request.cachePolicy = NSURLRequestCachePolicy.UseProtocolCachePolicy
        request.HTTPMethod = "POST"
        return request
    }

    func connection(connection: NSURLConnection, didReceiveData data: NSData) {

        print("Connection Data= \(NSString(data: data, encoding: NSUTF8StringEncoding))")
    }

    func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
        print("Connection Received Resopnse Headers= \((response as! NSHTTPURLResponse).allHeaderFields)")
    }

    func connection(connection: NSURLConnection, willCacheResponse cachedResponse: NSCachedURLResponse) -> NSCachedURLResponse? {
        print("Connection will cache Response")       
        return cachedResponse
    }
}

After removing must-revalidate from the header it was still fetching the request.

Connection Received Resopnse Headers= [Cache-Control: max-age=10, Transfer-Encoding: Identity, Date: Sun, 13 Sep 2015 18:35:43 GMT, Content-Type: application/json;charset=UTF-8, Server: Apache-Coyote/1.1, Expires: Sun, 13 Sep 2015 18:35:53 GMT]

Later findings show the POST request does get cached, but does not work like GET, where max-age is considered.

func startDownload(){
    let serverRequest = getServerURL()
    let cache = NSURLCache.sharedURLCache()
    let response = cache.cachedResponseForRequest(serverRequest)

    if response != nil {
        serverRequest.cachePolicy = NSURLRequestCachePolicy.ReturnCacheDataDontLoad
    }

    NSURLConnection(request: serverRequest, delegate: self, startImmediately: true)
}

Solution

  • tl;dr

    You need to use GET instead of POST.

    Lengthy Explanation

    The issue is that you're request is a POST.

    func getServerURL() -> NSMutableURLRequest{
        ...
        request.HTTPMethod = "POST"
        ...
    }
    

    In general, POST requests are used to create (or sometimes also to update) a resource on the server. Reusing the cached response for a creation or update request doesn't make much sense because you have to send the request to the server anyway (otherwise nothing is going to be created or updated). It seems that iOS automatically circumvents the cache on POST requests.

    In your particular case, however, you don't really need the POST because you're merely reading data from the server. That means you should use a GET request instead.

    func getServerURL() -> NSMutableURLRequest{
        ...
        request.HTTPMethod = "GET"
        ...
    }
    

    I verified that the iOS system actually reuses the cache with the following snippet.

    let d = HTTPJSONDonwload()
    
    // Initial request. Can not reuse cache.
    d.startDownload()
    
    // Subsequent request after 5 seconds. Should be able to reuse the cache.
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(5 * NSEC_PER_SEC)), dispatch_get_main_queue()) {
        d.startDownload()
    }
    
    // Subsequent request after 11 seconds. Cannot reuse the cache because
    // the expiration timeout is 10 seconds.
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(11 * NSEC_PER_SEC)), dispatch_get_main_queue()) {
        d.startDownload()
    }
    

    When I run this in the simulator and monitor the network calls with Charles Proxy, I indeed only see two events:

    Screenshot of Charles Proxy showing 2 requests

    The first call is the initial request

    Screenshot of Charles Proxy showing timing of first request

    and the second call is the third request which was issued after a delay of 11 seconds.

    Screenshot of Charles Proxy showing timing of second request

    Note that the second request, which was issued after 5 seconds, does not appear which means that the response was retrieved from the cache. The delegate methods of NSURLConnection will, however, still be called just as if the response came from the network. With the logging output in your code you'll, therefore, see all three requests on the console.

    Connection Received Resopnse Headers= [Server: Apache-Coyote/1.1, Content-Type: application/json;charset=UTF-8, Keep-Alive: timeout=15, max=100, Proxy-Connection: Keep-alive, Date: Mon, 14 Sep 2015 06:28:05 GMT, Content-Encoding: gzip, Content-Length: 36, Cache-Control: max-age=10, Vary: Accept-Encoding]
    Connection Data= Optional({"1":"New York"})
    Connection will cache Response
    Connection Received Resopnse Headers= [Server: Apache-Coyote/1.1, Content-Type: application/json;charset=UTF-8, Keep-Alive: timeout=15, max=100, Proxy-Connection: Keep-alive, Date: Mon, 14 Sep 2015 06:28:05 GMT, Content-Encoding: gzip, Content-Length: 36, Cache-Control: max-age=10, Vary: Accept-Encoding]
    Connection Data= Optional({"1":"New York"})
    Connection Received Resopnse Headers= [Server: Apache-Coyote/1.1, Content-Type: application/json;charset=UTF-8, Keep-Alive: timeout=15, max=99, Proxy-Connection: Keep-alive, Date: Mon, 14 Sep 2015 06:28:16 GMT, Content-Encoding: gzip, Content-Length: 36, Cache-Control: max-age=10, Vary: Accept-Encoding]
    Connection Data= Optional({"1":"New York"})
    Connection will cache Response
    

    Note that there is no Connection will cache Response after the second request because the response was retrieved from the cache and there is no point in caching it again.