6

I want my Service Worker to behave just like a browser cache in some situations. That means when responding with a cache hit, I first need to make sure that the resource is not expired. I can do this like this for example:

const cacheControl = response.headers.get('cache-control');
const date = new Date(response.headers.get('date'));
const age = parseInt(response.headers.get('age') || '0', 10);

const maxAge = getMaxAge(cacheControl);
const expiration = date.getTime() + 1000 * (maxAge - age);

const isFresh = Date.now() < expiration;

I get the cache-control, date and age header from the cached response, compute the expiration and compare to the current time.

The only problem with this approach is that it can be subject to clock drift between client and server. This is because the date header is generated on the server side but the final comparison is made with the local client time.

Imagin the client time is off by one day (which sadly happens sometimes), now the cache entries may be cached a day longer or shorter than intended.

My desired solution would be to add the fetch time as a custom header when storing the response in the cache. Then I could use this custom header instead of the

const networkResponse = fetch(request);
// does not work, headers are immutable
networkResponse.headers.append('x-time-fetched', Date.now());
cach.put(request, networkResponse);

Sadly, this solution does not work because the network response is not mutable. Btw: copying the response to add this additional information is not an option.

Does someone have an idea how to correctly identify stale cache entries like the browser?

Erik
  • 2,888
  • 2
  • 18
  • 35
  • 2
    Currently my best idea is to store the clock drift in the service worker and keep it up to date. The good thing is that the clock drift is the same for all responses from the same server. – Erik Jun 13 '18 at 09:53

1 Answers1

0

Sadly, this solution does not work because the network response is not mutable. Btw: copying the response to add this additional information is not an option.

This is actually quite doable.

One solution to this would be to create a new Response object and add the new header manually.

const newHeaders = new Headers()
for (let entry of response.headers.entries()) {
  headers.append(entry[0], entry[1])
}
headers.append('x-time-fetched', Date.now())

const newResponse = await response.blob().then(blob => {
  return new Response(blob, {
    status: response.status,
    statusText: res.statusText,
    headers: headers
  })
})

Another simpler but hacky solution would be to just create a new property on the response object instead of depending on the headers object:

response.__time_fetched = new Date()
fny
  • 31,255
  • 16
  • 96
  • 127
  • Hey @fny, thanks for the reply. Unfortunately, the **first** solution copies the response when accessing `.blob()`. Chrome, for example, supports streams to circumvent this problem but Firefox and Edge don't ( see https://stackoverflow.com/questions/49251501/new-response-from-stream-in-microsoft-edge ). The **second** solution does not work either: when you add a custom attribute to the response and store it in the service worker cache it will be dropped :( – Erik Jun 26 '18 at 16:49
  • Agh. Let me ponder this more. I might come up with something clever... keeping track of drift seems to be the next best option, although from my experience, that path has pains of its own... – fny Jun 27 '18 at 00:10