Service Worker Routing โ Part 4: The safeRequest() Helper ๐งน
Bonus: The safeRequest() Helper โ Refactoring the Router ๐งน
After the router was working, Kyle noticed the same pattern repeating everywhere:
- Check cache first (sometimes)
- Try network if online
- Cache the response (sometimes)
- Check cache as fallback (sometimes)
Rather than copy-paste that logic six times with slight variations, he extracted it into a configurable helper:
async function safeRequest(reqURL, request, opts = {}) {
let {
cacheResponse = false,
checkCacheFirst = false,
checkCacheLast = false,
useRequestDirectly = false
} = opts;
let cache = await caches.open(cacheName);
// 1. Check cache first (if requested)
if (checkCacheFirst) {
let res = await cache.match(reqURL);
if (res) return res;
}
// 2. Try network if online
if (isOnline) {
try {
let res = useRequestDirectly
? await fetch(request) // POST โ pass request object directly
: await fetch(request.url, opts); // GET โ pass URL + options
// Handle opaque redirects (status === 0) โ let browser handle, don't cache
if (res.ok || res.status === 0) {
if (cacheResponse && res.status !== 0) {
await cache.put(reqURL, res.clone());
}
return res;
}
} catch(err) {}
}
// 3. Check cache as fallback (if requested)
if (checkCacheLast) {
let res = await cache.match(reqURL);
if (res) return res;
}
}How Each Resource Type Uses It
// API calls โ check cache last, handle caching manually (GET only), use request directly (POST support)
safeRequest(reqURL, request, {
cacheResponse: false, // handle caching manually
checkCacheFirst: false,
checkCacheLast: true,
useRequestDirectly: true // needed for POST body access
});
// HTML pages โ network first, cache last, cache conditionally (skip X-Not-Found)
safeRequest(reqURL, request, {
cacheResponse: false, // cache manually to check X-Not-Found header
checkCacheFirst: false,
checkCacheLast: true,
useRequestDirectly: false
});
// Static assets โ cache first, network fallback, auto-cache response
safeRequest(reqURL, request, {
cacheResponse: true,
checkCacheFirst: true,
checkCacheLast: false,
useRequestDirectly: false
});Two Nuances Worth Noting
1. useRequestDirectly for POST requests
When making a POST, the body lives on the request object. Passing just request.url and options loses the body. The workaround is to pass the full request object directly to fetch(). Kyle mentioned this might be a browser bug โ either way, this is the reliable solution.
2. Opaque redirects have status === 0
When the server returns a redirect (e.g. /logout โ /), the SW sees a response with status: 0 โ an opaque redirect. You can't inspect it or cache it. Let it pass through to the browser unchanged. Checking res.ok || res.status === 0 handles both successful responses and pass-through redirects.
"Instead of repeating this over and over again in some combination, I just wrote a little thing that can be configured with Boolean flags to do any variation of the strategy I need."
The router logic stays readable, the repetition is gone, and adding a new route type only means one new call to safeRequest with the right flags.