Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bypassing http response header Cache-Control: how to set cache expiration?

All http responses from a server come with the headers that inform our app not to cache the responses:

Cache-Control: no-cache
Pragma: no-cache
Expires: 0

So, if you are making NSUrlRequests with default cache policy "NSURLRequestUseProtocolCachePolicy" then the app will always load data from the server. However, we need to cache the responses and the obvious solution would be to set these headers to some time for example (at backend side), set to 10 seconds. But I'm interested in a solution how to bypass this policy and cache every request for 10 seconds.

For that you need to setup shared cache. That might be done in AppDelegate didFinishLaunchingWithOptions:

NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                             diskCapacity:20 * 1024 * 1024
                                               diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];

Then, we need to embed our code to force to cache a response. If you use an instance of AFHttpClient then it can be done by overriding the method below and manually storing the cache into the shared cache:

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
              willCacheResponse:(NSCachedURLResponse *)cachedResponse {

  NSMutableDictionary *mutableUserInfo = [[cachedResponse userInfo] mutableCopy];
  NSMutableData *mutableData = [[cachedResponse data] mutableCopy];
  NSURLCacheStoragePolicy storagePolicy = NSURLCacheStorageAllowedInMemoryOnly;

  // ...

  return [[NSCachedURLResponse alloc] initWithResponse:[cachedResponse response]
                                                data:mutableData
                                            userInfo:mutableUserInfo
                                       storagePolicy:storagePolicy];
}

And the last thing is to set cachePolicy for the requests. In our case we want to set the same cache policy to all requests. So again, if you use an instance of AFHttpClient then it can be done by overriding the method below:

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {

  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;

  return request;
}

So far so good. "NSURLRequestReturnCacheDataElseLoad" makes to perform request first time and load response from cache all other times. The problem is, it's unclear how to set cache expiration time, for example 10 seconds.

like image 646
Centurion Avatar asked Nov 07 '13 16:11

Centurion


2 Answers

You can implement a custom NSURLCache that only returns cached responses that has not expired.

Example:

#import "CustomURLCache.h"

NSString * const EXPIRES_KEY = @"cache date";
int const CACHE_EXPIRES = -10;

@implementation CustomURLCache

// static method for activating this custom cache
+(void)activate {
    CustomURLCache *urlCache = [[CustomURLCache alloc] initWithMemoryCapacity:(2*1024*1024) diskCapacity:(2*1024*1024) diskPath:nil] ;
    [NSURLCache setSharedURLCache:urlCache];
}

-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
    NSCachedURLResponse * cachedResponse = [super cachedResponseForRequest:request];
    if (cachedResponse) {
        NSDate* cacheDate = [[cachedResponse userInfo] objectForKey:EXPIRES_KEY];
        if ([cacheDate timeIntervalSinceNow] < CACHE_EXPIRES) {
            [self removeCachedResponseForRequest:request];
            cachedResponse = nil;
        }
    }

    return cachedResponse;
}

- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
    NSMutableDictionary *userInfo = cachedResponse.userInfo ? [cachedResponse.userInfo mutableCopy] : [NSMutableDictionary dictionary];
    [userInfo setObject:[NSDate date] forKey:EXPIRES_KEY];
    NSCachedURLResponse *newCachedResponse = [[NSCachedURLResponse alloc] initWithResponse:cachedResponse.response data:cachedResponse.data userInfo:userInfo storagePolicy:cachedResponse.storagePolicy];

    [super storeCachedResponse:newCachedResponse forRequest:request];
}

@end

If this does not give you enough control then I would implement a custom NSURLProtocol with a startLoading method as below and use it in conjunction with the custom cache.

- (void)startLoading
{
    NSMutableURLRequest *newRequest = [self.request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:@"CacheSet" inRequest:newRequest];

    NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
    if (cachedResponse) {  
        [self connection:nil didReceiveResponse:[cachedResponse response]];
        [self connection:nil didReceiveData:[cachedResponse data]];
        [self connectionDidFinishLoading:nil];
    } else {
        _connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
    }
}

Some links:

  • Useful info on NSURLCache
  • Creating a custom NSURLProtocol
like image 130
Stephanus Mostert Avatar answered Oct 28 '22 23:10

Stephanus Mostert


If somebody would be interested, here is Stephanus answer rewritten in Swift:

class CustomURLCache: NSURLCache {

    // UserInfo expires key
    let kUrlCacheExpiresKey = "CacheData";

    // How long is cache data valid in seconds
    let kCacheExpireInterval:NSTimeInterval = 60*60*24*5;

    // get cache response for a request
    override func cachedResponseForRequest(request:NSURLRequest) -> NSCachedURLResponse? {
        // create empty response
        var response:NSCachedURLResponse? = nil

        // try to get cache response
        if let cachedResponse = super.cachedResponseForRequest(request) {

            // try to get userInfo
            if let userInfo = cachedResponse.userInfo {

                // get cache date
                if let cacheDate = userInfo[kUrlCacheExpiresKey] as NSDate? {

                    // check if the cache data are expired
                    if (cacheDate.timeIntervalSinceNow < -kCacheExpireInterval) {
                        // remove old cache request
                        self.removeCachedResponseForRequest(request);
                    } else {
                        // the cache request is still valid
                        response = cachedResponse
                    }
                }
            }
        }

        return response;
    }

    // store cached response
    override func storeCachedResponse(cachedResponse: NSCachedURLResponse, forRequest: NSURLRequest) {
        // create userInfo dictionary
        var userInfo = NSMutableDictionary()
        if let cachedUserInfo = cachedResponse.userInfo {
            userInfo = NSMutableDictionary(dictionary:cachedUserInfo)
        }
        // add current date to the UserInfo
        userInfo[kUrlCacheExpiresKey] = NSDate()

        // create new cached response
        let newCachedResponse = NSCachedURLResponse(response:cachedResponse.response, data:cachedResponse.data, userInfo:userInfo,storagePolicy:cachedResponse.storagePolicy)
        super.storeCachedResponse(newCachedResponse, forRequest:forRequest)

    }

}
like image 29
Vojtech Vrbka Avatar answered Oct 29 '22 00:10

Vojtech Vrbka