Handling broken images with the service worker
A few years ago, I wrote about how we can use css to style broken images. The technique leveraged on the fact that any styling to the ::before
or ::after
pseudo-elements on the <img>
element will only be applied if the image doesn’t load. So, we could style those elements and they would only display if the image was broken.
Here’s an example of how I’ve styled broken images on this site:
There are pros and cons to handling broken images this way. One limitation is browser support, as this technique doesn’t work in some major browsers like Safari.
Having recently done a lot of work with service workers, it occurred to me that we could use the service worker to handle broken images in a different way. Since the service worker can tell if an image file isn’t able to be fetched, we can handle that condition by, for example, serving a different image to the browser.
Intercepting requests for broken images #
In the service worker fetch
event, we can tell if and when a request the browser makes goes wrong, whether that be because the user is offline or because the response to the fetch request was bad.
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
// User is online, but response was not ok
})
.catch((err) => {
// User is probably offline
})
)
});
In either of these scenarios, we can check to see if the failed request was for an image and do whatever we like in response.
function isImage(fetchRequest) {
return fetchRequest.method === "GET"
&& fetchRequest.destination === "image";
}
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
// User is online, but response was not ok
if (isImage(e.request)) {
// do something
}
})
.catch((err) => {
// User is probably offline
if (isImage(e.request)) {
// do something
}
})
)
});
Serving a “broken image” image #
One way to handle requests for images that don’t resolve would be to send a placeholder image in its place. For example, we can have an image like the one below to show the user that the image is broken.
We can implement this by responding to the fetch
request with a new request for the palceholder file, /broken.png
.
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
// User is online, but response was not ok
if (isImage(e.request)) {
// Fetch the broken image placeholder instead
return fetch("/broken.png");
}
})
.catch((err) => {
// User is probably offline
if (isImage(e.request)) {
// do something
}
}) // end fetch
)
});
This will work when the user is online, but if we want this to also work offline, we will need to cache the placeholder image. This is typically done during the install
phase of the service worker lifecycle.
self.addEventListener('install', (e) => {
self.skipWaiting();
e.waitUntil(
caches.open("precache").then((cache) => {
// Add /broken.png to "precache"
cache.add("/broken.png");
})
);
});
Once the image is in the cache, we can respond to the fetch
request with a response from the cache instead of having to make a new request over the network.
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request)
.then((response) => {
if (response.ok) return response;
// User is online, but response was not ok
if (isImage(e.request)) {
// Get broken image placeholder from cache
return caches.match("/broken.png");
}
})
.catch((err) => {
// User is probably offline
if (isImage(e.request)) {
// Get broken image placeholder from cache
return caches.match("/broken.png");
}
})
)
});
I created a GitHub Gist with the full implementation in case you want to use it in your own projects.