2
votes

I'm using NSURLSession to request a JSON resource from an HTTP server. The server uses Cache-Control to limit the time the resource is cached on clients.

This works great, but I'd also like to cache a deserialized JSON object in memory as it is accessed quite often, while continuing to leverage the HTTP caching mechanisms built into NSURLSession.

I'm thinking I can save a few HTTP response headers: Content-MD5, Etag, and Last-Modified along with the deserialized JSON object (I'm using those 3 fields since I've noticed not all HTTP servers return Content-MD5, otherwise that'd be sufficient by itself). The next time I receive a response for the JSON object, if those 3 fields are the same then I can reuse the previously deserialized JSON object.

Is this a robust way to determine the deserizlied JSON is still valid. If not, how do I determine if the deserialized object is up to date?

2

2 Answers

6
votes

I created a HTTPEntityFingerprint structure which stores some of the entity headers: Content-MD5, Etag, and Last-Modified.

import Foundation

struct HTTPEntityFingerprint {
    let contentMD5 : String?
    let etag : String?
    let lastModified : String?
}

extension HTTPEntityFingerprint {
    init?(response : NSURLResponse) {
        if let httpResponse = response as? NSHTTPURLResponse {
            let h = httpResponse.allHeaderFields
            contentMD5 = h["Content-MD5"] as? String
            etag = h["Etag"] as? String
            lastModified = h["Last-Modified"] as? String

            if contentMD5 == nil && etag == nil && lastModified == nil {
                return nil
            }
        } else {
            return nil
        }
    }

    static func match(first : HTTPEntityFingerprint?, second : HTTPEntityFingerprint?) -> Bool {
        if let a = first, b = second {
            if let md5A = a.contentMD5, md5B = b.contentMD5 {
                return md5A == md5B
            }
            if let etagA = a.etag, etagB = b.etag {
                return etagA == etagB
            }
            if let lastA = a.lastModified, lastB = b.lastModified {
                return lastA == lastB
            }
        }

        return false
    }
}

When I get an NSHTTPURLResponse from an NSURLSession, I create an HTTPEntityFingerprint from it and compare it against a previously stored fingerprint using HTTPEntityFingerprint.match. If the fingerprints match, then the HTTP resource hasn't changed and thus I do not need to deserialized the JSON response again; however, if the fingerprints do not match, then I deserialize the JSON response and save the new fingerprint.

This mechanism only works if your server returns at least one of the 3 entity headers: Content-MD5, Etag, or Last-Modified.

More Details on NSURLSession and NSURLCache Behavior

The caching provided by NSURLSession via NSURLCache is transparent, meaning when you request a previously cached resource NSURLSession will call the completion handlers/delegates as if a 200 response occurred.

If the cached response has expired then NSURLSession will send a new request to the origin server, but will include the If-Modified-Since and If-None-Match headers using the Last-Modified and Etag entity headers in the cached (though expired) result; this behavior is built in, you don't have to do anything besides enable caching. If the origin server returns a 304 (Not Modified), then NSURLSession will transform this to a 200 response the application (making it look like you fetched a new copy of the resource, even though it was still served from the cache).

2
votes

This could be done with simple HTTP standard response.

Assume previous response is something like below:

{ status code: 200, headers {
    "Accept-Ranges" = bytes;
    Connection = "Keep-Alive";
    "Content-Length" = 47616;
    Date = "Thu, 23 Jul 2015 10:47:56 GMT";
    "Keep-Alive" = "timeout=5, max=100";
    "Last-Modified" = "Tue, 07 Jul 2015 11:28:46 GMT";
    Server = Apache;
} }

Now use below to tell server not to send date if it is not modified since.

NSURLSession is a configurable container, you would probably need to use http option "IF-Modified-Since"

Use below configuration kind before downloading the resource,

    NSURLSessionConfiguration *backgroundConfigurationObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"myBackgroundSessionIdentifier"];
    [backgroundConfigurationObject setHTTPAdditionalHeaders:
     @{@"If-Modified-Since": @"Tue, 07 Jul 2015 11:28:46 GMT"}];

if resource for example doesn't change from above set date then below delegate will be called

    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
    {

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) downloadTask.response;
        if([httpResponse statusCode] == 304)
                //resource is not modified since last download date
    }

Check the downloadTask.response status code is 304 .. then resource is not modified and the resource is not downloaded.

Note save the previous success full download date in some NSUserDefaults to set it in if-modifed-since