diff --git a/ui/public/sw.js b/ui/public/sw.js index c5f43d34ae0..213c029362e 100644 --- a/ui/public/sw.js +++ b/ui/public/sw.js @@ -50,6 +50,15 @@ self.addEventListener("fetch", (event) => { return; } + // Vite development modules use the browser's conditional-response cache. + // Passing their requests through fetch/respondWith can return a bodyless 304 + // to the module loader on reload and leave the app root empty. They are not + // build assets and have no offline contract, so leave them to the browser. + if (url.origin === self.location.origin && + /^\/(?:@fs|@vite|@id|src|node_modules)\//.test(url.pathname)) { + return; + } + // Network-first; only public build assets can use an offline fallback. event.respondWith( fetch(request) diff --git a/ui/src/lib/service-worker-cache-policy.test.ts b/ui/src/lib/service-worker-cache-policy.test.ts index 92116966484..01911911059 100644 --- a/ui/src/lib/service-worker-cache-policy.test.ts +++ b/ui/src/lib/service-worker-cache-policy.test.ts @@ -22,6 +22,17 @@ function worker() { } describe("service worker privacy boundaries", () => { + it.each([ + "/@fs/tmp/runner/vite-cache/deps/@assistant-ui_react.js?v=fixture", + "/@vite/client", "/@id/__x00__virtual:module", "/src/main.tsx", "/node_modules/.vite/deps/react.js", + ])("leaves conditional Vite module request %s to the browser", pathname => { + const w = worker(); + w.fetch.mockResolvedValue(new Response(null, { status: 304 })); + expect(w.request("default", pathname)).not.toHaveBeenCalled(); + expect(w.fetch).not.toHaveBeenCalled(); + expect(w.put).not.toHaveBeenCalled(); + expect(w.match).not.toHaveBeenCalled(); + }); it("bypasses both caching and offline fallback for a no-store request outside /api", () => { const w = worker(); expect(w.request("no-store")).not.toHaveBeenCalled();