Ben Copsey

Rework expiry handling in ASIDownloadCache:

All requests now store a single expiry timestamp constructed from either secondsToCache, a cache-control: max-age header, or an expires header (in order of precedence)
This is used for all expiry checks, and should speed up checking if cached data is stale because it elimiates the need for NSDateFormatter or NSScanner when reading from the cache
(closes gh-173)
@@ -35,11 +35,6 @@ @@ -35,11 +35,6 @@
35 // A helper function that determines if the server has requested data should not be cached by looking at the request's response headers 35 // A helper function that determines if the server has requested data should not be cached by looking at the request's response headers
36 + (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request; 36 + (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request;
37 37
38 -// A date formatter that can be used to construct an RFC 1123 date  
39 -// The returned formatter is safe to use on the calling thread  
40 -// Do not use this formatter for parsing dates because the format can vary slightly - use ASIHTTPRequest's dateFromRFC1123String: class method instead  
41 -+ (NSDateFormatter *)rfc1123DateFormatter;  
42 -  
43 @property (assign, nonatomic) ASICachePolicy defaultCachePolicy; 38 @property (assign, nonatomic) ASICachePolicy defaultCachePolicy;
44 @property (retain, nonatomic) NSString *storagePath; 39 @property (retain, nonatomic) NSString *storagePath;
45 @property (retain) NSRecursiveLock *accessLock; 40 @property (retain) NSRecursiveLock *accessLock;
@@ -101,17 +101,39 @@ static NSString *permanentCacheFolder = @"PermanentStore"; @@ -101,17 +101,39 @@ static NSString *permanentCacheFolder = @"PermanentStore";
101 101
102 NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; 102 NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request];
103 NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request]; 103 NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request];
104 - 104 +
105 NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]]; 105 NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]];
106 if ([request isResponseCompressed]) { 106 if ([request isResponseCompressed]) {
107 [responseHeaders removeObjectForKey:@"Content-Encoding"]; 107 [responseHeaders removeObjectForKey:@"Content-Encoding"];
108 } 108 }
109 - if (maxAge != 0) { 109 +
110 - [responseHeaders removeObjectForKey:@"Expires"]; 110 + // Create a special 'X-ASIHTTPRequest-Expires' header
111 - [responseHeaders setObject:[NSString stringWithFormat:@"max-age=%i",(int)maxAge] forKey:@"Cache-Control"]; 111 + // This is what we use for deciding if cached data is current, rather than parsing the expires / max-age headers individually each time
  112 + // We store this as a timestamp to make reading it easier as NSDateFormatter is quite expensive
  113 +
  114 + // If we weren't given a custom max-age, lets look for one in the response headers
  115 + if (!maxAge) {
  116 + NSString *cacheControl = [[responseHeaders objectForKey:@"Cache-Control"] lowercaseString];
  117 + if (cacheControl) {
  118 + NSScanner *scanner = [NSScanner scannerWithString:cacheControl];
  119 + [scanner scanUpToString:@"max-age" intoString:NULL];
  120 + if ([scanner scanString:@"max-age" intoString:NULL]) {
  121 + [scanner scanString:@"=" intoString:NULL];
  122 + [scanner scanDouble:&maxAge];
  123 + }
  124 + }
  125 + }
  126 +
  127 + // RFC 2612 says max-age must override any Expires header
  128 + if (maxAge) {
  129 + [responseHeaders setObject:[NSNumber numberWithDouble:[[[NSDate date] addTimeInterval:maxAge] timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
  130 + } else {
  131 + NSString *expires = [responseHeaders objectForKey:@"Expires"];
  132 + if (expires) {
  133 + [responseHeaders setObject:[NSNumber numberWithDouble:[[ASIHTTPRequest dateFromRFC1123String:expires] timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"];
  134 + }
112 } 135 }
113 - // We use this special key to help expire the request when we get a max-age header 136 +
114 - [responseHeaders setObject:[[[self class] rfc1123DateFormatter] stringFromDate:[NSDate date]] forKey:@"X-ASIHTTPRequest-Fetch-date"];  
115 [responseHeaders writeToFile:headerPath atomically:NO]; 137 [responseHeaders writeToFile:headerPath atomically:NO];
116 138
117 if ([request responseData]) { 139 if ([request responseData]) {
@@ -281,33 +303,10 @@ static NSString *permanentCacheFolder = @"PermanentStore"; @@ -281,33 +303,10 @@ static NSString *permanentCacheFolder = @"PermanentStore";
281 303
282 if ([self shouldRespectCacheControlHeaders]) { 304 if ([self shouldRespectCacheControlHeaders]) {
283 305
284 - // Look for a max-age header 306 + // Look for X-ASIHTTPRequest-Expires header to see if the content is out of date
285 - NSString *cacheControl = [[cachedHeaders objectForKey:@"Cache-Control"] lowercaseString]; 307 + NSNumber *expires = [cachedHeaders objectForKey:@"X-ASIHTTPRequest-Expires"];
286 - if (cacheControl) {  
287 - NSScanner *scanner = [NSScanner scannerWithString:cacheControl];  
288 - [scanner scanUpToString:@"max-age" intoString:NULL];  
289 - if ([scanner scanString:@"max-age" intoString:NULL]) {  
290 - [scanner scanString:@"=" intoString:NULL];  
291 - NSTimeInterval maxAge = 0;  
292 - [scanner scanDouble:&maxAge];  
293 -  
294 - NSDate *fetchDate = [ASIHTTPRequest dateFromRFC1123String:[cachedHeaders objectForKey:@"X-ASIHTTPRequest-Fetch-date"]];  
295 - NSDate *expiryDate = [[[NSDate alloc] initWithTimeInterval:maxAge sinceDate:fetchDate] autorelease];  
296 -  
297 - if ([expiryDate timeIntervalSinceNow] >= 0) {  
298 - [[self accessLock] unlock];  
299 - return YES;  
300 - }  
301 - // RFC 2612 says max-age must override any Expires header  
302 - [[self accessLock] unlock];  
303 - return NO;  
304 - }  
305 - }  
306 -  
307 - // Look for an Expires header to see if the content is out of date  
308 - NSString *expires = [cachedHeaders objectForKey:@"Expires"];  
309 if (expires) { 308 if (expires) {
310 - if ([[ASIHTTPRequest dateFromRFC1123String:expires] timeIntervalSinceNow] >= 0) { 309 + if ([[NSDate dateWithTimeIntervalSince1970:[expires doubleValue]] timeIntervalSinceNow] >= 0) {
311 [[self accessLock] unlock]; 310 [[self accessLock] unlock];
312 return YES; 311 return YES;
313 } 312 }
@@ -408,21 +407,6 @@ static NSString *permanentCacheFolder = @"PermanentStore"; @@ -408,21 +407,6 @@ static NSString *permanentCacheFolder = @"PermanentStore";
408 return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]]; 407 return [NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],result[8], result[9], result[10], result[11],result[12], result[13], result[14], result[15]];
409 } 408 }
410 409
411 -+ (NSDateFormatter *)rfc1123DateFormatter  
412 -{  
413 - NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];  
414 - NSDateFormatter *dateFormatter = [threadDict objectForKey:@"ASIDownloadCacheDateFormatter"];  
415 - if (dateFormatter == nil) {  
416 - dateFormatter = [[[NSDateFormatter alloc] init] autorelease];  
417 - [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];  
418 - [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];  
419 - [dateFormatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss 'GMT'"];  
420 - [threadDict setObject:dateFormatter forKey:@"ASIDownloadCacheDateFormatter"];  
421 - }  
422 - return dateFormatter;  
423 -}  
424 -  
425 -  
426 - (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request 410 - (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request
427 { 411 {
428 // Ensure the request is allowed to read from the cache 412 // Ensure the request is allowed to read from the cache