One of the Mobile Web Specialist students I’m mentoring came across the issue of caching remote resources from her Service Worker and I think it’s worth a little explanation. The course project includes some resources fetched from an external service and the application was throwing an exception, as these resources were not cached. It’s actually two issues, but let’s see what happens when we try to cache items from a remote origin.

Caching in general

I made a small post about implementing caching using a Service Worker before, feel free to read it to get some insights. For now just let’s take a look at the function which implements the caching:

What happens is, the ServiceWorker (SW) listens to (and interrupts) the fetch events originating from within its scope. These events are triggered when the browser downloads something or anything. In this simple form of caching, the SW checks if the request is already stored in any cache and if it is, it will return the stored response. If there is no cached response, it will go to the network, send a new request (this now will not be interrupted, that would result in an infinite loop) and once a response was received, cache a clone of it and return the original response.

Now we want to focus on remote resources, so let’s say it downloads a JavaScript file from a CDN. And then comes the surprise, you will notice, that the file was successfully downloaded, but not cached. And it’s all because of line 5.

Caching from remote origin

It’s fairly common to check the response status and only cache successful responses. This can be done by checking the Response.status for a desired status code or simple checking the Response.ok. response.ok is basically the same as writing response.status >= 200 && response.status < 300, looks better, but maybe not so clean at first sight what it means. When you send a normal fetch request like above to a remote origin, independent from the actual result, the response.status will be 0 and the response.ok will be false.

The opaque request

Generally speaking we don’t want to allow JavaScript to access content from remote websites. There are many articles about this, search for CORS,  Access-Control-Allow-Origin and CSRF. When the remote website doesn’t explicitly allow our JavaScript application to access its resources, we can still send a fetch request and actually cache the response. However, this request will be a so called opaque request.

An opaque request is sent from a ServiceWorker on a remote origin (to oversimplify, let’s say from a different domain) without explicitly asking for CORS or Cross-Origin Resource Sharing. In the above code example, the default Request object has a (read-only) property called mode and its value is set to ‘no-cors‘. The SW will be allowed to send this request, will be allowed to put it in the cache and on request, return it from the cache. However, the SW should not know anything else about the response, will not even receive the HTTP status code.

CORS or no-CORS

And it might be totally fine. You can just blindly cache whatever you received, just want to make sure, you keep caching the latest response (not like in the above example, where if something is cached, the cache is not updated). This way if the visitor goes offline, all the resources, even the remote ones, like for example some fonts stored on CDN are available in the cache.

It might be possible though, that you want to know if the response was successful. For example you are accessing your API server or you have some flaky resource and you don’t want to overwrite your cache with some bogus data or a temporary “error 500” page. In this case the request is better made with the mode set to ‘cors‘ and as long as the remote server enabled Access-Control-Allow-Origin for your origin, your SW will receive the response just as if it was local. Again, to do this CORS access has to be enabled on the remote server.

A friendly warning: you don’t want to randomly send CORS requests. While an opaque request will go through to a remote resource, if the mode is set to cors and the remote server doesn’t allow access from your origin, the request will fail, it will not just fallback and become an opaque request.

Fetching with cors

As mentioned before, the mode property is read-only. Once the Request object is created, you are not supposed to modify it. This will not work:

I mean, it will work, just not as you would expect it by looking at it. The mode will not be actually updated, if the request was originally sent with no-cors, it will still be no-cors when sending the new fetch request.

I didn’t explicitly craft a new Request object, but calling fetch with the URL and this init parameter effectively does the same. As long as remote.com allowed this to go through, the response.status will represent the real HTTP status.

Handling errors in fetch

Not strictly related to this topic, but the ServiceWorker which I looked into failed in offline mode for two reasons. One was, that it checked the response.ok value of fetching the remote resource, even though it made an opaque request. As explained above, for these it will be always false, therefore these requests were not cached.

The other issue was, that if a request wasn’t cached and the fetch failed (which it will if the visitor is offline, that’s kind of guaranteed unless magic is involved) it didn’t handle the failure. In this case, even though most of the page could have been rendered using the cached resources, the browser’s offline page will be shown, rendering all our caching useless.

So let’s go back to the original code snippet and update it.

There it is. Now, if the resource is not cached and fetching it fails, the page will still load, except for the parts which were not cached. This example will simply show the error as a text, but you can also just pre-cache a custom error page while caching the application shell and return that custom error page from the cache.




Tagged with