Cannot seem to fetch cached response with Alamofire

Viewed 463

I am trying to cache a JSON response (https://api.github.com/search/repositories) with the help of Alamofire's ResponseCacher with a expiry of 2 hours in the client. In one of the posts in SO, Jon of Alamofire had suggested to modify the CachedURLResponse in the ResponseCacher. The response does not have a Cache-Control or any cache headers. So I am manually adding the cache-control header in the response. I have also put together the session configuration code from my understanding, however I can't seem to get the cache working with the overridden expiry. The session definition is as follows:

let sessionConfiguration = URLSessionConfiguration.af.default
sessionConfiguration.requestCachePolicy = .useProtocolCachePolicy
sessionConfiguration.urlCache = URLCache(memoryCapacity: 20 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024)

let responseCacher = ResponseCacher(behavior: .modify { _, response in
  return CachedURLResponse(
    response: response.response,
    data: response.data,
    userInfo: userInfo,
    storagePolicy: .allowed)
    .response(with: 7200)
})

let aSession = Session(configuration: sessionConfiguration, cachedResponseHandler: responseCacher)

where the response modifier is defined as follows

extension CachedURLResponse {
    func response(with expirationDuration: Int) -> CachedURLResponse {
        var cachedResponse = self
        if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String : String], let url = httpResponse.url{

            headers["Cache-Control"] = "private, max-age=\(expirationDuration)"
            headers.removeValue(forKey: "Expires")
            headers.removeValue(forKey: "s-maxage")

            if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) {
            cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy)
            }
        }
        return cachedResponse
    }
}

If I set the requestCachePolicy to .useProtocolCachePolicy, when network is offline, I don't get any cached response. If I set the requestCachePolicy to .returnCacheDataElseLoad the cache never expires. I need to cache my response, but also have it expire after 2 hours and return the cached response when network is offline.

I am beginner when it comes to caching so there are places I have made some leaps without completely understanding how it works. Parts of the code are taken from SO.

1 Answers

The first thing that handles the caching is the URLCache. You need to override the default URLCache and force the caching by adding the cache control headers.

Something like this should work:

let sessionConfiguration = URLSessionConfiguration.af.default
sessionConfiguration.urlCache = ForceCache.makeForceCache()

let aSession = Session(configuration: sessionConfiguration)

/// The server does not return proper cache control headers for caching so we override it here
private class ForceCache: URLCache {

    static func makeForceCache() -> URLCache {
        let memoryCapacity = 20 * 1_024 * 1_024
        // According to https://developer.apple.com/documentation/foundation/nsurlsessiondatadelegate/1411612-urlsession?language=objc
        // The response size is small enough to reasonably fit within the cache.
        // (For example, if you provide a disk cache, the response must be no larger than
        // about 5% of the disk cache size.)
        let diskCapacity = 400 * 1_024 * 1_024
        let path = "org.something.something"

        return ForceCache(memoryCapacity: memoryCapacity,
                          diskCapacity: diskCapacity,
                          diskPath: path)
    }

    private let constSecondsToKeepOnDisk = 2 * 60 * 60 // 2 hours

    override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
        var customCachedResponse = cachedResponse
        // Set custom Cache-Control
        if let response = cachedResponse.response as? HTTPURLResponse,
           var newHeaders = response.allHeaderFields as? [String: String] {
            newHeaders["Cache-Control"] = "public, max-age=\(constSecondsToKeepOnDisk)"
            newHeaders["Expires"] = nil
            newHeaders["s-maxage"] = nil

            if let url = response.url,
               // Consider only specific request
               url.absoluteString.contains("search/repositories"),
               let newResponse = HTTPURLResponse(url: url,
                                                 statusCode: response.statusCode,
                                                 httpVersion: "HTTP/1.1", headerFields: newHeaders) {
                customCachedResponse = CachedURLResponse(response: newResponse,
                                                         data: cachedResponse.data,
                                                         userInfo: cachedResponse.userInfo,
                                                         storagePolicy: cachedResponse.storagePolicy)
            }
        }
        super.storeCachedResponse(customCachedResponse, for: request)
    }
}

For completeness I should add that normally caching should be handled by the server. If the server sends that you should not cache the request that means there is a good reason behind that decision (or a design mistake).

Related