{"record":{"id":"95f8a39389452001","repo":"GraphiteEditor/Graphite","slug":"network-request-failed-and-no-cache-available-for","errorCode":null,"errorMessage":"Network request failed and no cache available for ${request.url}","messagePattern":"Network request failed and no cache available for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/service-worker.js","lineNumber":46,"sourceCode":"\tconst cache = await caches.open(cacheName);\n\tconst cached = await cache.match(request);\n\tif (cached) return cached;\n\n\tconst response = await fetch(request);\n\tif (isCacheable(response)) cache.put(request, response.clone());\n\treturn response;\n}\n\nasync function networkFirst(request, cacheName) {\n\tconst cache = await caches.open(cacheName);\n\ttry {\n\t\tconst response = await fetch(request);\n\t\tif (isCacheable(response)) cache.put(request, response.clone());\n\t\treturn response;\n\t} catch {\n\t\tconst cached = await cache.match(request);\n\t\tif (cached) return cached;\n\t\tthrow new Error(`Network request failed and no cache available for ${request.url}`);\n\t}\n}\n\n// ================\n// Lifecycle events\n// ================\n\nself.addEventListener(\"install\", (event) => {\n\tevent.waitUntil(\n\t\t(async () => {\n\t\t\t// Precache app shell assets\n\t\t\tconst cache = await caches.open(STATIC_CACHE_NAME);\n\t\t\tawait Promise.all(\n\t\t\t\tPRECACHE_MANIFEST.map(async (entry) => {\n\t\t\t\t\tconst response = await fetch(entry.url);\n\t\t\t\t\tif (!response.ok) throw new Error(`Precache fetch failed for ${entry.url}: ${response.status}`);\n\t\t\t\t\t// Strip the `redirected` flag which causes errors when served via respondWith\n\t\t\t\t\tconst cleaned = response.redirected","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/frontend/src/service-worker.js#L28-L64","documentation":"Thrown by the Graphite service worker's networkFirst strategy, which is used only for the font catalog API (https://api.graphite.art/font-list). It fires when the live fetch() rejects (offline, DNS failure, API unreachable) AND the runtime-fonts cache holds no previously stored response for that URL. The rejection propagates out of event.respondWith, so the page sees a failed request for the font list.","triggerScenarios":"DevTools offline throttling or a genuinely offline machine combined with a font list that was never cached: the install-time prefetch of FONT_LIST_API is best-effort (wrapped in try/catch, only cached when response.ok), so a failed first fetch leaves the runtime-fonts cache empty and every later offline request hits this throw.","commonSituations":"Testing the PWA offline right after a fresh install where the font catalog prefetch failed; api.graphite.art blocked by an extension, firewall, or corporate proxy; a first response that was 4xx/5xx (isCacheable() refuses to store non-ok, non-opaque responses, so no offline fallback ever gets seeded).","solutions":["Bring the browser online and load the font list once so networkFirst caches a good response, then retest offline","Catch the rejection in the fetch handler and return a 503 Response with an empty font list instead of letting it fail","Verify the cache name passed to networkFirst (RUNTIME_FONTS) matches the cache the install-time prefetch writes to","Check that a proxy, extension, or DNS issue is not blocking api.graphite.art"],"exampleFix":"// before (service-worker.js networkFirst)\n} catch {\n\tconst cached = await cache.match(request);\n\tif (cached) return cached;\n\tthrow new Error(`Network request failed and no cache available for ${request.url}`);\n}\n\n// after: degrade gracefully instead of throwing\n} catch {\n\tconst cached = await cache.match(request);\n\tif (cached) return cached;\n\treturn new Response(\"{\\\"fonts\\\":[]}\", { status: 503, headers: { \"Content-Type\": \"application/json\" } });\n}","handlingStrategy":"fallback","validationCode":"// Check whether the font catalog is cached before relying on it offline\nasync function fontListCached(): Promise<boolean> {\n\tconst cache = await caches.open(\"runtime-fonts\");\n\treturn Boolean(await cache.match(\"https://api.graphite.art/font-list\"));\n}","typeGuard":null,"tryCatchPattern":"// In the SW fetch handler, catch the networkFirst rejection and degrade gracefully\nevent.respondWith(\n\tnetworkFirst(request, RUNTIME_FONTS).catch(\n\t\t() => new Response(\"{\\\"fonts\\\":[]}\", { status: 503, headers: { \"Content-Type\": \"application/json\" } }),\n\t),\n);","preventionTips":["Keep the install-time font catalog prefetch healthy — it is the only thing that seeds the offline fallback","Design the font picker UI to tolerate a failed or empty font list response","Whenever adding a networkFirst route, pair it with a cached or synthetic fallback response"],"tags":["service-worker","offline","cache","pwa","fonts","fetch"],"backgroundTag":"service-worker-offline-cache-miss","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}