Service Worker Routing โ€” Part 4: The safeRequest() Helper ๐Ÿงน

March 5, 20262 min read
service workerPWAofflinecachingweb workersprogressive web appsbackground sync

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.