1

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)
}
NNikN
  • 3,322
  • 5
  • 38
  • 74
  • Is the problem that the `willCacheResponse` delegate method isn't called or that subsequent requests don't use the cached response but instead reload from the server? – hennes Sep 13 '15 at 17:36
  • Previously I was using NSURLSession for which willCacheReponse was not called. Now, I am using NSURLConnection, for which willCacheResponse is CALLED. But, it is not considering the age which is 10 seconds and fetches every time from the server. – NNikN Sep 13 '15 at 18:08
  • Did you try to monitor the actual network calls? When you start the request again within the ten seconds, it should issue a `HEAD` request to check whether the resource has changed. Could it be that your server is indicating that the resource has changed in its response to the `HEAD` request? – hennes Sep 13 '15 at 18:13
  • Server does not indicate resource has changed, its a fixed json response for testing. If the request is sent within 10 seconds, then the client iOS itself should return the data from the cache, as I do have expires and max-age details. Do you know a place where I can deploy the .war and you can check it from your side, as u already have client code. – NNikN Sep 13 '15 at 18:21
  • I think iOS will **always** do a `HEAD` request because your server returns `must-revalidate` in the `Cache-Control` header field. The documentation says: *If the contents must be revalidated, the URL loading system makes a HEAD request to the originating source to see if the resource has changed. If it has not changed, then the URL loading system returns the cached response. If it has changed, the URL loading system fetches the data from the originating source.* I suspect this is where it goes wrong. Does it make any difference if you remove `must-revalidate`? – hennes Sep 13 '15 at 18:30
  • [This](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html) is the link to Apple's docs. I couldn't fit it into the last comment. And sorry, I don't know a place where you could quickly deploy your .war. – hennes Sep 13 '15 at 18:31
  • @hennes Added a working url in the code, so now you can just copy the client code and see if there is any issue you can help to find. – NNikN Sep 13 '15 at 23:39
  • Any luck getting it to work with the GET? – hennes Sep 15 '15 at 06:12

1 Answers1

1

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.

hennes
  • 8,542
  • 3
  • 40
  • 59
  • GET works fine. Thanks. But, with POST I found, it does cache but does not consider the age. I have added this in my question. – NNikN Sep 15 '15 at 11:40
  • @andyPaul I noticed that as well. It might be that it ignores the specified expiration because it automatically assumes POST requests can't or shouldn't be cached. In that case it could also skip the caching in the first place, however. Unfortunately, I was unable to find any official documentation covering the handling of GET and POST requests. – hennes Sep 15 '15 at 16:30