Search code examples
ioshttp-headersnsurlsessionnsurlcachenshttpurlresponse

How to use NSURLSession to determine if resource has changed?


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?


Solution

  • 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).