Important changes to download cache behaviour:
ASIDownloadCache will now store 301, 302, 303 and 307 redirect responses in the cache. Requests pulling from the cache will automatically redirect (assuming shouldRedirect is true) as if they'd received a Location header from the server. This change should allow the download cache to operate more effectively as an offline fallback when no internet connection is available. Additionally, requests encountering a 304 will no longer read from the cache and then write back to it. Instead, they merely update the expiry date (if an updated date was supplied). As part of this work, two new required methods were added to the ASICacheDelegate protocol, see ASICacheDelegate.h for more info Finally, ASIDownloadCache now stores the response status code in a custom header in the cached headers dictionary, and requests will have their response status code set to this when they are pulled from the cache.
Showing
5 changed files
with
149 additions
and
25 deletions
| @@ -56,6 +56,13 @@ typedef enum _ASICacheStoragePolicy { | @@ -56,6 +56,13 @@ typedef enum _ASICacheStoragePolicy { | ||
| 56 | // Should return the cache policy that will be used when requests have their cache policy set to ASIUseDefaultCachePolicy | 56 | // Should return the cache policy that will be used when requests have their cache policy set to ASIUseDefaultCachePolicy |
| 57 | - (ASICachePolicy)defaultCachePolicy; | 57 | - (ASICachePolicy)defaultCachePolicy; |
| 58 | 58 | ||
| 59 | +// Returns the date a cached response should expire on. Pass a non-zero max age to specify a custom date. | ||
| 60 | +- (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge; | ||
| 61 | + | ||
| 62 | +// Updates cached response headers with a new expiry date. Pass a non-zero max age to specify a custom date. | ||
| 63 | +- (void)updateExpiryForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge; | ||
| 64 | + | ||
| 65 | +// Looks at the request's cache policy and any cached headers to determine if the cache data is still valid | ||
| 59 | - (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request; | 66 | - (BOOL)canUseCachedDataForRequest:(ASIHTTPRequest *)request; |
| 60 | 67 | ||
| 61 | // Removes cached data for a particular request | 68 | // Removes cached data for a particular request |
| @@ -85,31 +85,24 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | @@ -85,31 +85,24 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | ||
| 85 | [[self accessLock] unlock]; | 85 | [[self accessLock] unlock]; |
| 86 | } | 86 | } |
| 87 | 87 | ||
| 88 | -- (void)storeResponseForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge | 88 | +- (void)updateExpiryForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge |
| 89 | { | 89 | { |
| 90 | - [[self accessLock] lock]; | 90 | + NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; |
| 91 | - | 91 | + NSMutableDictionary *cachedHeaders = [NSMutableDictionary dictionaryWithContentsOfFile:headerPath]; |
| 92 | - if ([request error] || ![request responseHeaders] || ([request responseStatusCode] != 200) || ([request cachePolicy] & ASIDoNotWriteToCacheCachePolicy)) { | 92 | + if (!cachedHeaders) { |
| 93 | - [[self accessLock] unlock]; | ||
| 94 | return; | 93 | return; |
| 95 | } | 94 | } |
| 96 | - | 95 | + NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge]; |
| 97 | - if ([self shouldRespectCacheControlHeaders] && ![[self class] serverAllowsResponseCachingForRequest:request]) { | 96 | + if (!expires) { |
| 98 | - [[self accessLock] unlock]; | ||
| 99 | return; | 97 | return; |
| 100 | } | 98 | } |
| 99 | + [cachedHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; | ||
| 100 | + [cachedHeaders writeToFile:headerPath atomically:NO]; | ||
| 101 | +} | ||
| 101 | 102 | ||
| 102 | - NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; | 103 | +- (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge |
| 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]) { | ||
| 107 | - [responseHeaders removeObjectForKey:@"Content-Encoding"]; | ||
| 108 | - } | ||
| 109 | - | ||
| 110 | - // Create a special 'X-ASIHTTPRequest-Expires' header | ||
| 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 | 106 | ||
| 114 | // If we weren't given a custom max-age, lets look for one in the response headers | 107 | // If we weren't given a custom max-age, lets look for one in the response headers |
| 115 | if (!maxAge) { | 108 | if (!maxAge) { |
| @@ -126,13 +119,62 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | @@ -126,13 +119,62 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | ||
| 126 | 119 | ||
| 127 | // RFC 2612 says max-age must override any Expires header | 120 | // RFC 2612 says max-age must override any Expires header |
| 128 | if (maxAge) { | 121 | if (maxAge) { |
| 129 | - [responseHeaders setObject:[NSNumber numberWithDouble:[[[NSDate date] addTimeInterval:maxAge] timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; | 122 | + return [[NSDate date] addTimeInterval:maxAge]; |
| 130 | } else { | 123 | } else { |
| 131 | NSString *expires = [responseHeaders objectForKey:@"Expires"]; | 124 | NSString *expires = [responseHeaders objectForKey:@"Expires"]; |
| 132 | if (expires) { | 125 | if (expires) { |
| 133 | - [responseHeaders setObject:[NSNumber numberWithDouble:[[ASIHTTPRequest dateFromRFC1123String:expires] timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; | 126 | + return [ASIHTTPRequest dateFromRFC1123String:expires]; |
| 127 | + } | ||
| 128 | + } | ||
| 129 | + return nil; | ||
| 130 | +} | ||
| 131 | + | ||
| 132 | +- (void)storeResponseForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterval)maxAge | ||
| 133 | +{ | ||
| 134 | + [[self accessLock] lock]; | ||
| 135 | + | ||
| 136 | + if ([request error] || ![request responseHeaders] || ([request cachePolicy] & ASIDoNotWriteToCacheCachePolicy)) { | ||
| 137 | + [[self accessLock] unlock]; | ||
| 138 | + return; | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + // We only cache 200/OK or redirect reponses (redirect responses are cached so the cache works better with no internet connection) | ||
| 142 | + int responseCode = [request responseStatusCode]; | ||
| 143 | + if (responseCode != 200 && responseCode != 301 && responseCode != 302 && responseCode != 303 && responseCode != 307) { | ||
| 144 | + [[self accessLock] unlock]; | ||
| 145 | + return; | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + if ([self shouldRespectCacheControlHeaders] && ![[self class] serverAllowsResponseCachingForRequest:request]) { | ||
| 149 | + [[self accessLock] unlock]; | ||
| 150 | + return; | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + NSString *headerPath = [self pathToStoreCachedResponseHeadersForRequest:request]; | ||
| 154 | + NSString *dataPath = [self pathToStoreCachedResponseDataForRequest:request]; | ||
| 155 | + | ||
| 156 | + NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionaryWithDictionary:[request responseHeaders]]; | ||
| 157 | + if ([request isResponseCompressed]) { | ||
| 158 | + [responseHeaders removeObjectForKey:@"Content-Encoding"]; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + // Create a special 'X-ASIHTTPRequest-Expires' header | ||
| 162 | + // This is what we use for deciding if cached data is current, rather than parsing the expires / max-age headers individually each time | ||
| 163 | + // We store this as a timestamp to make reading it easier as NSDateFormatter is quite expensive | ||
| 164 | + | ||
| 165 | + NSDate *expires = [self expiryDateForRequest:request maxAge:maxAge]; | ||
| 166 | + if (expires) { | ||
| 167 | + [responseHeaders setObject:[NSNumber numberWithDouble:[expires timeIntervalSince1970]] forKey:@"X-ASIHTTPRequest-Expires"]; | ||
| 134 | } | 168 | } |
| 169 | + | ||
| 170 | + // Store the response code in a custom header so we can reuse it later | ||
| 171 | + | ||
| 172 | + // We'll change 304/Not Modified to 200/OK because this is likely to be us updating the cached headers with a conditional GET | ||
| 173 | + int statusCode = [request responseStatusCode]; | ||
| 174 | + if (statusCode == 304) { | ||
| 175 | + statusCode = 200; | ||
| 135 | } | 176 | } |
| 177 | + [responseHeaders setObject:[NSNumber numberWithInt:[request responseStatusCode]] forKey:@"X-ASIHTTPRequest-Response-Status-Code"]; | ||
| 136 | 178 | ||
| 137 | [responseHeaders writeToFile:headerPath atomically:NO]; | 179 | [responseHeaders writeToFile:headerPath atomically:NO]; |
| 138 | 180 | ||
| @@ -282,15 +324,16 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | @@ -282,15 +324,16 @@ static NSString *permanentCacheFolder = @"PermanentStore"; | ||
| 282 | return NO; | 324 | return NO; |
| 283 | } | 325 | } |
| 284 | 326 | ||
| 285 | - // If we already have response headers for this request, check to see if the new content is different | ||
| 286 | - if ([request responseHeaders]) { | ||
| 287 | - | ||
| 288 | // New content is not different | 327 | // New content is not different |
| 289 | if ([request responseStatusCode] == 304) { | 328 | if ([request responseStatusCode] == 304) { |
| 290 | [[self accessLock] unlock]; | 329 | [[self accessLock] unlock]; |
| 291 | return YES; | 330 | return YES; |
| 292 | } | 331 | } |
| 293 | 332 | ||
| 333 | + // If we already have response headers for this request, check to see if the new content is different | ||
| 334 | + // We check [request complete] so that we don't end up comparing response headers from a redirection with these | ||
| 335 | + if ([request responseHeaders] && [request complete]) { | ||
| 336 | + | ||
| 294 | // If the Etag or Last-Modified date are different from the one we have, we'll have to fetch this resource again | 337 | // If the Etag or Last-Modified date are different from the one we have, we'll have to fetch this resource again |
| 295 | NSArray *headersToCompare = [NSArray arrayWithObjects:@"Etag",@"Last-Modified",nil]; | 338 | NSArray *headersToCompare = [NSArray arrayWithObjects:@"Etag",@"Last-Modified",nil]; |
| 296 | for (NSString *header in headersToCompare) { | 339 | for (NSString *header in headersToCompare) { |
This diff is collapsed. Click to expand it.
| @@ -14,6 +14,7 @@ | @@ -14,6 +14,7 @@ | ||
| 14 | @interface ASIDownloadCacheTests () | 14 | @interface ASIDownloadCacheTests () |
| 15 | - (void)runCacheOnlyCallsRequestFinishedOnceTest; | 15 | - (void)runCacheOnlyCallsRequestFinishedOnceTest; |
| 16 | - (void)finishCached:(ASIHTTPRequest *)request; | 16 | - (void)finishCached:(ASIHTTPRequest *)request; |
| 17 | +- (void)runRedirectTest; | ||
| 17 | @end | 18 | @end |
| 18 | 19 | ||
| 19 | 20 | ||
| @@ -330,14 +331,16 @@ | @@ -330,14 +331,16 @@ | ||
| 330 | 331 | ||
| 331 | - (void)test304 | 332 | - (void)test304 |
| 332 | { | 333 | { |
| 334 | + NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"]; | ||
| 335 | + | ||
| 333 | // Test default cache policy | 336 | // Test default cache policy |
| 334 | [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; | 337 | [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; |
| 335 | [[ASIDownloadCache sharedCache] setDefaultCachePolicy:ASIUseDefaultCachePolicy]; | 338 | [[ASIDownloadCache sharedCache] setDefaultCachePolicy:ASIUseDefaultCachePolicy]; |
| 336 | - ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"]]; | 339 | + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url]; |
| 337 | [request setDownloadCache:[ASIDownloadCache sharedCache]]; | 340 | [request setDownloadCache:[ASIDownloadCache sharedCache]]; |
| 338 | [request startSynchronous]; | 341 | [request startSynchronous]; |
| 339 | 342 | ||
| 340 | - request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"]]; | 343 | + request = [ASIHTTPRequest requestWithURL:url]; |
| 341 | [request setDownloadCache:[ASIDownloadCache sharedCache]]; | 344 | [request setDownloadCache:[ASIDownloadCache sharedCache]]; |
| 342 | [request startSynchronous]; | 345 | [request startSynchronous]; |
| 343 | BOOL success = ([request responseStatusCode] == 200); | 346 | BOOL success = ([request responseStatusCode] == 200); |
| @@ -348,6 +351,30 @@ | @@ -348,6 +351,30 @@ | ||
| 348 | 351 | ||
| 349 | success = ([[request responseData] length]); | 352 | success = ([[request responseData] length]); |
| 350 | GHAssertTrue(success,@"Response was empty"); | 353 | GHAssertTrue(success,@"Response was empty"); |
| 354 | + | ||
| 355 | + // Test 304 updates expiry date | ||
| 356 | + url = [NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/content_not_modified_but_expires_tomorrow"]; | ||
| 357 | + request = [ASIHTTPRequest requestWithURL:url]; | ||
| 358 | + [request setDownloadCache:[ASIDownloadCache sharedCache]]; | ||
| 359 | + [request startSynchronous]; | ||
| 360 | + | ||
| 361 | + NSTimeInterval expiryTimestamp = [[[[ASIDownloadCache sharedCache] cachedResponseHeadersForURL:url] objectForKey:@"X-ASIHTTPRequest-Expires"] doubleValue]; | ||
| 362 | + | ||
| 363 | + // Wait to give the expiry date a chance to change | ||
| 364 | + sleep(2); | ||
| 365 | + | ||
| 366 | + request = [ASIHTTPRequest requestWithURL:url]; | ||
| 367 | + [request setCachePolicy:ASIAskServerIfModifiedCachePolicy]; | ||
| 368 | + [request setDownloadCache:[ASIDownloadCache sharedCache]]; | ||
| 369 | + [request startSynchronous]; | ||
| 370 | + | ||
| 371 | + success = [request didUseCachedResponse]; | ||
| 372 | + GHAssertTrue(success, @"Cached data should have been used"); | ||
| 373 | + | ||
| 374 | + NSTimeInterval newExpiryTimestamp = [[[[ASIDownloadCache sharedCache] cachedResponseHeadersForURL:url] objectForKey:@"X-ASIHTTPRequest-Expires"] doubleValue]; | ||
| 375 | + NSLog(@"%@",[request responseString]); | ||
| 376 | + success = (newExpiryTimestamp > expiryTimestamp); | ||
| 377 | + GHAssertTrue(success, @"Failed to update expiry timestamp on 304"); | ||
| 351 | } | 378 | } |
| 352 | 379 | ||
| 353 | - (void)testStringEncoding | 380 | - (void)testStringEncoding |
| @@ -433,4 +460,50 @@ | @@ -433,4 +460,50 @@ | ||
| 433 | requestsFinishedCount++; | 460 | requestsFinishedCount++; |
| 434 | } | 461 | } |
| 435 | 462 | ||
| 463 | +- (void)testRedirect | ||
| 464 | +{ | ||
| 465 | + // Run this request on the main thread to force delegate calls to happen synchronously | ||
| 466 | + [self performSelectorOnMainThread:@selector(runRedirectTest) withObject:nil waitUntilDone:YES]; | ||
| 467 | +} | ||
| 468 | + | ||
| 469 | +- (void)runRedirectTest | ||
| 470 | +{ | ||
| 471 | + [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICacheForSessionDurationCacheStoragePolicy]; | ||
| 472 | + [[ASIDownloadCache sharedCache] clearCachedResponsesForStoragePolicy:ASICachePermanentlyCacheStoragePolicy]; | ||
| 473 | + [[ASIDownloadCache sharedCache] setDefaultCachePolicy:ASIUseDefaultCachePolicy]; | ||
| 474 | + [ASIHTTPRequest setDefaultCache:[ASIDownloadCache sharedCache]]; | ||
| 475 | + | ||
| 476 | + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/cached-redirect"]]; | ||
| 477 | + [request startSynchronous]; | ||
| 478 | + | ||
| 479 | + BOOL success = ([[[request url] absoluteString] isEqualToString:@"http://allseeing-i.com/i/logo.png"]); | ||
| 480 | + GHAssertTrue(success,@"Request did not redirect correctly, cannot proceed with test"); | ||
| 481 | + | ||
| 482 | + requestRedirectedWasCalled = NO; | ||
| 483 | + request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/cached-redirect"]]; | ||
| 484 | + [request setDelegate:self]; | ||
| 485 | + [request startSynchronous]; | ||
| 486 | + | ||
| 487 | + success = ([request didUseCachedResponse]); | ||
| 488 | + GHAssertTrue(success,@"Failed to cache final response"); | ||
| 489 | + | ||
| 490 | + GHAssertTrue(requestRedirectedWasCalled,@"Failed to call requestRedirected"); | ||
| 491 | +} | ||
| 492 | + | ||
| 493 | +- (void)requestRedirected:(ASIHTTPRequest *)redirected | ||
| 494 | +{ | ||
| 495 | + requestRedirectedWasCalled = YES; | ||
| 496 | +} | ||
| 497 | + | ||
| 498 | +- (void)request:(ASIHTTPRequest *)request willRedirectToURL:(NSURL *)newURL | ||
| 499 | +{ | ||
| 500 | + BOOL success = ([[newURL absoluteString] isEqualToString:@"http://allseeing-i.com/i/logo.png"]); | ||
| 501 | + GHAssertTrue(success,@"Request did not redirect correctly, cannot proceed with test"); | ||
| 502 | + | ||
| 503 | + success = ([request didUseCachedResponse]); | ||
| 504 | + GHAssertTrue(success,@"Failed to cache redirect response"); | ||
| 505 | + | ||
| 506 | + [request redirectToURL:newURL]; | ||
| 507 | +} | ||
| 508 | + | ||
| 436 | @end | 509 | @end |
-
Please register or login to post a comment