Seemingly erroneous cache behavior after "Cache-Control: max-age=N" expires #219

hiltonc opened this Issue Jul 14, 2011 · 7 comments


None yet
4 participants

hiltonc commented Jul 14, 2011

I have a resource being served from S3. When the resource is requested, it supplies a "Cache-Control: max-age=N" header. Using the default cache policies, ASIDownloadCache caches this properly, serving from the cache for the duration of N seconds. However, after that time has concluded, it makes a HEAD request, discovers that the resource has not changed, and uses the cached data, but does not update the "X-ASIHTTPRequest-Expires" cached header. This means every subsequent request, after the initial max-age has passed, will always check with the server before using the cached data.

Now, it is entirely possible that I do not understand the exact semantics of the "Cache-Control: max-age=N" header in this situation, but it seems to me that the cache's "X-ASIHTTPRequest-Expires" for this request should be updated another N seconds from "now" if the resource has not changed.

I will be happy to supply a patch for this issue (which I am working on fixing), but I want to verify that I correctly understand the semantics of the "Cache-Control: max-age=N" header in this situation.

hiltonc added a commit to hiltonc/asi-http-request that referenced this issue Jul 14, 2011

hiltonc commented Jul 14, 2011

You can see in the fork how I would fix this issue. Does this make general sense, or am I just working around a bug in S3 (i.e., that they should send the "Cache-Control: max-age=N" header in their response to the HEAD request as well)? It seems like this would be generally useful, even in that case, as this is the kind of default behavior you would want.


pokeb commented Aug 6, 2011

I've done a quick test, and as far as I can see the cache seems to be working for me as expected. Here are the steps I used to reproduce:

  1. Create a request using the cache, set a custom expiry time (eg 1/2 second).
  2. Create an identical request after the first has expired, ASIHTTPRequest will see that there is a matching cached response, but that it is no longer current, and will perform a conditional GET.
  3. In readResponseHeaders, the second request will hit the following:
if ([self downloadCache] && ([[self downloadCache] canUseCachedDataForRequest:self])) {

    // Update the expiry date
    [[self downloadCache] updateExpiryForRequest:self maxAge:[self secondsToCache]];

    // Read the response from the cache
    [self useDataFromCache];


canUseCachedDataForRequest will note that it got a 304, and will return YES, which should cause updateExpiryForRequest:maxAge: to be called.

There is a test for this behaviour (the method 'test304' in ASIDownloadCacheTests.m).

If you can share some code that demonstrates this not working, I'd be happy to take a look.



hiltonc commented Aug 6, 2011

Thank you for taking a look at this. This is a admittedly a corner case (although it happens to be the main use case of an app I'm building), but I have been able to write a unit test to demonstrate it.

In order to demonstrate the problem, I had to instrument ASIHTTPRequest by adding an ivar called didPerformConditionalGET. I modified the class to set the ivar to YES when parsing response headers and choosing to use cached data. This code should be executed when a cache entry has expired, and the class performs a conditional GET to see if the cached data is still good. If the cache entry is not expired, then this code should not be executed.

I then wrote a unit test called testMaxAgeAfterRecache. This method requests a file from S3 which has a max-age of 10s. The curious thing about S3 is that, when a full request is made, it will return the max-age header. But when a conditional GET request is made, it will not return the max-age header. This is the source of the problem I'm trying to fix with my change, as the code without my fix will not update the expiry date (as there is no max-age header in the response), which means that once the cache entry has expired, requests for that item will ALWAYS issue a conditional GET.


pokeb commented Aug 7, 2011

I haven't tested with your code yet, but is there an Etag header in the response from the conditional GET? If so, we can confirm if the content is still the same.

One way to solve this might be to add a X-ASIHTTPRequest-Fetch-Date header to the cache. In the absence of a new expiry date on a conditional GET, we could make up a new expiry time based on the time difference between X-ASIHTTPRequest-Fetch-Date and X-ASIHTTPRequest-Expires.

hiltonc commented Aug 23, 2011

Yes, it does respond with etag. These are the headers the conditional GET returns:

x-amz-id-2 = AT/6I11p7CH35av3s/Gd5H9dBw+3Sg587jh6BNa4NCv0S9T6rTXSvx4fVom16cK6
Last-Modified = Sat, 06 Aug 2011 14:56:58 GMT
Server = AmazonS3
x-amz-request-id = 9CEC0238D3EB4E75
Date = Tue, 23 Aug 2011 16:16:55 GMT
Etag = "c9bd9cb8b2831c59a2e6d03410698c14"

Had the fix merged into the trunk?


MihaiDamian commented Aug 23, 2012

I can see the same behaviour reported by hiltonc. If you get a 304 response X-ASIHTTPRequest-Expires is not updated anymore if the response contained no Cache-Control headers.

Of course it all boils down on how you interpret the HTTP protocol and whether you include Cache Control headers or not.
This is what RFC 2616 has to say:

The response MUST include the following header fields:

  • Expires, Cache-Control, and/or Vary, if the field-value might
    differ from that sent in any previous response for the same

Seems to me that Cache-Control may be omitted in some cases, even though many people choose to include it. hiltonc's fix seems to work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment