NSURLCache not working with NSURLSession
NSURLCache and NSURLSession currently appear to be buggy at best, and possibly broken. I don't need background downloads or anything like that, so I opted to use Cash: https://github.com/nnoble/Cash . It works perfectly for my needs.
How to cache using NSURLSession and NSURLCache. Not working
Note that the following SO post helped me solve my problem: Is NSURLCache persistent across launches?
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
// Set app-wide shared cache (first number is megabyte value)
NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:@"nsurlcache"];
sleep(1); // Critically important line, sadly, but it's worth it!
In addition to the
sleep(1) line, also note the size of my cache; 500MB.
According to docs you need a cache size that is way bigger than what you're trying to cache.
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.)
So for example if you want to be able to cache a 10MB image, then a cache size of 10MB or even 20MB will not be enough. You need 200MB.
Honey's comment below is evidence that Apple is following this 5% rule. For an 8Mb he had to set his cache size to minimum 154MB.
NSURLSession not using cached responses
I can't tell you with absolute certainty why the cache isn't being consulted, but I can give you a list of the most likely reasons:
- The server did not respond with 304 when queried about the validity of that ETag header (IIRC using a HEAD request).
- The request is too big—either relative to the size of the buffer or in absolute terms. The cache should at least be a couple of orders of magnitude bigger than the requests that you would typically cache; anything over about 5% of the cache size will not be cached.
- The request method is something other than GET. (Only GET requests are cached unless you monkey with the machinery significantly.)
- More than 10 minutes have elapsed (600 seconds isn't very long).
- The request was made in a different URL session that has a different backing cache.
- The request was made in an ephemeral URL session or a session that for some other reason has no cache.
- The session actually is returning the cached response, but you're seeing a request because it is revalidating a little more aggressively than you might expect—possibly because it will reach its maximum age so soon.
- Your URL request is getting handled in the background by a custom NSURLProtocol that doesn't respect the cache (e.g. because of some badly behaved third-party networking or advertising framework).
- The request had not actually been fully written to the cache when you tried to retrieve it (timing race caused by multiple threads).
I'm probably forgetting several others. With that said, if I'm forgetting them, that probably means that they aren't documented.
If you verify that everything listed above is working as expected, file a bug at bugreporter.apple.com and include enough code to reproduce the problem, along with a packet dump if possible.
NSURLCache, together with NSURLSession, does not respect: Cache-Control: max-age:86000, private, must-revalidate
The problem is the usage of the Cache-Control response directive must-revalidate.
By omitting must-revalidate you already have the perfect definition of your use case as far as I've understood it:
Cache-Control: max-age=86400, private
This controls how long the requested resource is considered fresh. After this time has elapsed, the answer should no longer come directly from the cache instead the server should be contacted for validation for subsequent requests. In your case since the server supplies an ETag, iOS sends a request with an If-None-Match header to the server.
To check this, I used your testRestfulAPI method without NSURLCache settings and configured a maximum age of 60 seconds on the server side, so I don't have to wait a day to check the result.
After that, I triggered testRestfulAPI once per second. I always got the desired result from the cache. And Charles showed that the data must come from the cache because the server was not contacted for 60 seconds.
Here is a quote from RFC 7234 (which obsoletes RFC 2616), under 126.96.36.199. it states:
The must-revalidate directive is necessary to support reliable
operation for certain protocol features. In all circumstances a cache
MUST obey the must-revalidate directive; in particular, if a cache
cannot reach the origin server for any reason, it MUST generate a 504
(Gateway Timeout) response.
The must-revalidate directive ought to be used by servers if and only
if failure to validate a request on the representation could result in
incorrect operation, such as a silently unexecuted financial
After reading that and if you put yourself in the view of a cache developer, you can well imagine that when a must-revalidate is seen, the original server is always contacted and any additional directives such as max-age are simply ignored. It seems to me that caches often show exactly this behavior in practice.
There is another section in chapter 188.8.131.52. which I will not conceal and which reads as follows:
The "must-revalidate" response directive indicates that once it has become stale, a cache MUST NOT use the response to satisfy subsequent requests without successful validation on the origin server.
This is often interpreted that by specifying max-age together with must-revalidate you can determine when a content is stale (after max-age seconds) and then it must validate at the origin server before it can serve the content.
In practice, however, for the reasons given above, it seems that must-revalidate always leads to a validation of each request on the origin server.
NSURLSession and image cache
NSURLSession uses shared NSURLCache to cache responses. If you want to limit disk/memory usage of a shared cache you should create a new cache and set it as a default one:
let URLCache = NSURLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 20 * 1024 * 1024, diskPath: nil)
You could find a little more about caching here.