I have an app which uses URLSession-based networking and URLCache for storing network requests on disk. I noticed that when the storage size of URLCache reaches the diskCapacity
, the eviction strategy seems to be to remove all entries, which is a problem in my use case. So I decided to write an URLCache subclass which would provide a custom storage for cached responses and which would implement LRU eviction strategy with better control.
As URLCache's documentation states, subclassing for this purpose should be supported:
The URLCache class is meant to be used as-is, but you can subclass it when you have specific needs. For example, you might want to screen which responses are cached, or reimplement the storage mechanism for security or other reasons.
However, I ran into problems with trying to use this new URLCache subclass with URLSession networking.
I have a test resource which I fetch using HTTP GET. The response headers contain:
Cache-Control: public, max-age=30
Etag: <some-value>
When using the standard, non-subclassed URLCache, the first request loads the data from network as expected (verified with HTTP proxy). The second request doesn't go to the network, if done within first 30 seconds, as expected. Subsequent requests after 30 seconds cause conditional GETs with Etag, as expected.
When using a URLCache subclass, all requests load the data from network - max-age doesn't seem to matter, and no conditional GETs are made.
It seems that the URLCache does something special to the CachedURLResponse
instances after they're loaded from its internal storage, and this something affects how URLSession handles the HTTP caching logic. What am I missing here?
I've written a very minimal URLCache implementation to demonstrate this problem. This class stores and loads CachedURLResponse
instances using NSKeyedArchiver
/ NSKeyedUnarchiver
, and it supports only zero or one response. Note that there are no calls to super
- this is by design, since I want to use my own storage.
Here's the implementation:
class CustomURLCache: URLCache {
let cachedResponseFileURL = URL(filePath: NSTemporaryDirectory().appending("entry.data"))
// MARK: Internal storage
func read() -> CachedURLResponse? {
guard let data = try? Data(contentsOf: cachedResponseFileURL) else { return nil }
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! CachedURLResponse
}
func store(_ cachedResponse: CachedURLResponse) {
try! (try! NSKeyedArchiver.archivedData(withRootObject: cachedResponse, requiringSecureCoding: false)).write(to: cachedResponseFileURL)
}
// MARK: URLCache Overrides
override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
read()
}
override func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: @escaping (CachedURLResponse?) -> Void) {
guard let response = read() else {
completionHandler(nil)
return
}
completionHandler(response)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
store(cachedResponse)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) {
store(cachedResponse)
}
}
My test case:
func test() {
let useEvictingCache = false
let config = URLSessionConfiguration.default
if useEvictingCache {
config.urlCache = CustomURLCache()
} else {
config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 1024 * 1024 * 100)
}
self.urlSession = URLSession(configuration: config)
let url = URL(string: "https://example.com/my-test-resource")!
self.urlSession?.dataTask(with: URLRequest(url: url), completionHandler: { data, response, error in
if let data {
print("GOT DATA with \(data.count) bytes")
} else if let error {
print("GOT ERROR \(error)")
}
}).resume()
}