How to know if NSURLSessionDataTask response came from cache? - ios

I would like to determine if the response from NSURLSessionDataTask came from cache, or was served from server
I'am creating my NSURLSessionDataTask from
request.cachePolicy = NSURLRequestUseProtocolCachePolicy;

Two easy options come to mind:
Call [[NSURLCache sharedURLCache] cachedResponseForRequest:request] before you make the request, store the cached response, then do that again after you finish receiving data, and compare the two cached responses to see if they are the same.
Make an initial request with the NSURLRequestReturnCacheDataDontLoad cache policy, and if that fails, make a second request with a more sane policy.
The first approach is usually preferable, as the second approach will return data that exists in the cache even if it is stale. However, in some rare cases (e.g. an offline mode), that might be what you want, which is why I mentioned it.

In order to know whether an URLSessionDataTask response comes from the cache or the network, you have to use a custom URLSession and provide it with an URLSessionTaskDelegate which has to implement the following method:
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
You'll find very useful information in metrics, especially the list of transactions metrics for the request. Each transaction metrics has a property resourceFetchType which can either be .localCache, .networklLoad, .serverPush or .unknown.
More information here: Apple documentation regarding URLSessionTaskDelegate

If your information need is only out of curiosity you can view your network usage the Xcode runtime network metics. Change the policy to a different setting and observe the difference.

If you want to use caching then it's not a good answer to disable caching on the client.
if the server has set cache headers (etag, cache-control="no-store") then NSURLSession will revalidate and serve you cached / fresh response based on the 200 / 304 response from the server. However in your code you will always see statusCode 200 regardless of if NSUrlSession received 200 or 304. This is limiting, because you may want to skip parsing, re-creating objects etc when the response hasnt changed.
THe workaround I did, is to use the etag value in the response to determine if something has changed
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response;
NSString *etag = (httpResp && [httpResp isKindOfClass:[NSHTTPURLResponse class]]) ? httpResp.allHeaderFields[#"Etag"] : nil;
BOOL somethingHasChanged = [etag isEqualToString:oldEtag];

Related

iOS: NSURLCache with NSURLSession alongside HTTP headers

NSURLCache is some kind of a dark art magic that is neither documented properly, nor behaves as expected.
I am using AFNetworking's AFHTTPSessionManager to utilise NSURLSession. I am using this mocking service to create custom HTTP responses.
I am setting the NSURLCache with disk capacity and all the cache policies.
Then in the URLSession:task:didCompleteWithError method of NSURLSessionTaskDelegate, I am doing this:
NSURLRequest *request = task.currentRequest;
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
The documentation states about the cachedResponseForRequest:
#result The NSCachedURLResponse stored in the cache with the given
request, or nil if there is no NSCachedURLResponse stored with the
given request.
AFNetworking's FAQ states:
So long as your NSURLRequest objects have the correct cache policy,
and your server response contains a valid Cache-Control header,
responses will be automatically cached for subsequent requests.
After all this my assumption is that the value of the the cachedResponse variable will be nil if the Cache-Control is NOT set properly. However, whatever I have tried to do so far - setting it to 'private', 'no-cache' or whatever - the cachedResponse ALWAYS contains the cached response. How can I verify that the caching mechanism works as expected ? Is there anything that I am missing from the setup ?
I have been doing my tests by firstly making the request online, then switching off the internet on my computer. XCode 7.0 beta iOS 9.0.
Any help is appreciated.

NSURLSession's NSURLCache not working as expected

I'm seeing weird behaviors when using NSURLCache and exhausted all options but posting here... so here it is:
I'm trying to implement NSURLCache as underlying caching mechanism in combination with NSURLSession. And using Charles and simple println() I observe that no matter what I do, my app continues to go back to the server and re-requests previous requests.
I validated that my server sends the following headers consistently:
Cache-Control:public, max-age=31449600
Content-Length:74289
Content-Type:image/jpeg
ETag:"abcdf"
Expires:Sun, 03 Jan 2016 20:58:01 GMT
Last-Modified:Sat, 09 Nov 2013 08:05:53 GMT
Yet when I make the following request:
let req = NSURLRequest(URL: NSURL(string: URLString)!, cachePolicy: .ReturnCacheDataElseLoad, timeoutInterval: 15)
NSURLSession's willCacheResponse: delegate method doesn't get called at all:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
However, when simply requesting NSURLRequest(URL: NSURL(string: URLString)!) without specifying specific cachePolicy, then willCacheResponse: does get called. Nevertheless, the next time the same request is made, the app goes back to server...
I don't do any response object modifications in any NSURLSession delegate methods and my requests are all HTTP GET.
What am I missing?
I've read a lot that's out there on NSURLCache:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/CachePolicies.html
http://nshipster.com/nsurlcache/
http://petersteinberger.com/blog/2012/nsurlcache-uses-a-disk-cache-as-of-ios5/
NSURLCache doesn't cache
various other SO threads...
Setup: Xcode6.1, Swift, iOS8+
Edit: I pushed a basic example to github to show my problem: https://github.com/opfeffer/nsurlcache
Check it out and let me know what I'm doing wrong!
I see your GET request has parameters, and for that I think we are facing the same situation.
I reached a solution overriding NSURLCache doing some adjustments and accessing the underlying DB, you can see it here:
https://github.com/JCardenete/NTURLCache/tree/master/NTURLCache
NSURLCache - Disk caching for GET request with parameters not working

AFNetworking 2.0 - Forced caching

Is it possible to force response caching if it contains neither Expires or Cache-Control: max-age?
I've came across this article, but unfortunately URLSession:dataTask:willCacheResponse:completionHandler: never gets called in my AFHTTPSessionManager subclass.
Any help appreciated.
You can force the caching by implementing your own NSURLProtocol that does not follow the standard HTTP caching rules. A complete tutorial is here, which persists the data using Core Data, but the basic steps are:
Subclass NSURLProtocol
Register your subclass with +registerClass:
Return YES in your +canInitWithRequest: method if this is the first time you've seen request, or NO if it isn't
You now have two choices:
Implement your own cache storage (in which case, follow the tutorial linked above)
Inject the cache control headers that you wish the URL loading system to follow
Assuming you want #2, override connection:didReceiveResponse: in your protocol subclass to create a response that has the cache control headers you want to emulate:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
// Create a dictionary with the headers you want
NSMutableDictionary *newHeaders = [response.allHeaderFields mutableCopy];
newHeaders[#"Cache-Control"] = #"no-transform,public,max-age=300,s-maxage=900";
// Create a new response
NSHTTPURLResponse *newResponse = [[NSHTTPURLResponse alloc] initWithURL:response.URL
statusCode:response.statusCode
HTTPVersion:#"HTTP/1.1"
headerFields:newHeaders];
[self.client URLProtocol:self
didReceiveResponse:newResponse
cacheStoragePolicy:NSURLCacheStorageAllowed];
}
This will cause the response to be cached as if the server had provided these headers.
For URL sessions only, you need to set the session configuration's protocolClasses. Since you're using AFNetworking, that looks like:
[AFHTTPSessionManager sharedManager].session.configuration.protocolClasses = #[[MyURLProtocol class]]
There are some caveats, so make sure you read the protocolClasses documentation.
A few notes:
If there's any way to fix this by having your server send the appropriate headers, please, please do that instead.
For the sake of brevity I hardcoded "HTTP/1.1", but technically you should pull this out of the response.
AFNetworking uses the standard URL Loading System, and is mostly unrelated to this issue.

How do I invalidate iOS's cache for a particular URL?

Using NSURLSession's default caching, how do I invalidate the cache for a particular URL?
I note NSURLCache's removeCachedResponseForRequest: method, but that takes an NSURLRequest object, which I don't have for the original request. Do I need to store those as I create them so I can then pass them back into removeCachedResponseForRequest: or can I just create a new one with the appropriate URL which will then serve as equivalent for the purpose, even if it doesn't have the same header fields and other properties as the original?
If you want to go further you could reset the cached response for the url request you want to force the reload. Doing the following:
let newResponse = NSHTTPURLResponse(URL: urlrequest.URL!, statusCode: 200, HTTPVersion: "1.1", headerFields: ["Cache-Control":"max-age=0"])
let cachedResponse = NSCachedURLResponse(response: newResponse!, data: NSData())
NSURLCache.sharedURLCache().storeCachedResponse(cachedResponse, forRequest: urlrequest)
As the cache-control header of the response hax a max age of 0 (forced) the response will never be returned when you do this request.
Your answer works fine for forcing a single request, but if you want to have two versions of the request one forcing and another relying on the cached response, removing the cached one once you force a request is desired.
The solution turns out not to be invalidating the cache for an existing URL, but to set:
request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
When you make the next request for the resource you know to be invalid. There are options to ignore the local cache only, or to request that upstream proxies ignore their caches too. See the NSURLRequest/NSMutableURLRequest documentation for details.
Here's what has been working for me:
request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
Here are all the options listed regarding chache policy, so you may find one that better suits your need:
Using Swift 2.2 and Xcode 7.3

How to use NSURLCache to return cached API responses when offline (iOS App)

I'm hoping someone can shed some light on a few things I've been researching but not making much progress on.
I'd like to take advantage of NSURLCache to return cached responses for API calls that I make within an iOS App. When the device is online, I'd like to return the cached response if it's recent enough, otherwise fetch from remote. When the device is offline I'd like to immediately return the cached response (if any) regardless of it's age.
I'm using AFNetworking. The API calls that I'm making are to a server I control. Protocol is HTTPS. The Cache-Control response header is currently "max-age=0, public". I'm not currently setting cache related request headers (should I?). I set the request's cache policy to be NSURLRequestReturnCacheDataDontLoad when offline and use the default NSURLRequestUseProtocolCachePolicy when online. And I can see the requests and their responses in the default Cache.db on disk. However when I'm offline all requests fail (no cached responses (despite appearing to have been cached) are being used/returned.
Per http://nshipster.com/nsurlcache/ I initialize a sharedURLCache in didFinishLaunchingWithOptions and set the AFNetworking setCacheResponse block to something like this:
NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy];
NSMutableData *mutableData = [[cachedResponse data] mutableCopy];
NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowed;
return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response] data:mutableData userInfo:mutableUserInfo storagePolicy:storagePolicy];
I've read these and other posts on the topic:
http://petersteinberger.com/blog/2012/nsurlcache-uses-a-disk-cache-as-of-ios5/
http://blackpixel.com/blog/2012/05/caching-and-nsurlconnection.html
I'm wondering if anyone out there has successfully achieved this functionality before using the standard NSURLCache (also interested in success stories involving SDURLCache but Peter S. says as of iOS5 disk caching is supported therefore SDURLCache is no longer needed, ie I'd like to use the default built in cache).
Thanks in advance!
Did you see this post?
AFNetworking (AFHttpClient) offline mode not working with NSURLRequestReturnCacheDataDontLoad policy
Looks like it might be a bug with iOS 6. Here is what Robert Mao had to say in the post:
A summarized work around is read the response from cache, instead of
use NSURLRequestReturnCacheDataDontLoad policy:
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cachedResponse != nil &&
[[cachedResponse data] length] > 0)
{
// Get cached data
....
}
Unless all of your calls are 100% GET and free of side effects or time dependency then this is dangerous.

Resources