From bbad277671288266c9d636a3f212db09d6e48fef Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Fri, 31 Jul 2026 22:56:17 +0800 Subject: [PATCH] fix(ui-server-auth): bust poisoned asset caches (#2196) --- apps/ui-server-auth/README.md | 2 +- apps/ui-server-auth/public/_headers | 5 +++-- .../src/cloudflare-pages-routing.test.ts | 8 ++++++++ apps/ui-server-auth/vite.config.ts | 15 +++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/ui-server-auth/README.md b/apps/ui-server-auth/README.md index b65769126..9411a43f9 100644 --- a/apps/ui-server-auth/README.md +++ b/apps/ui-server-auth/README.md @@ -23,7 +23,7 @@ pnpm -F @proj-airi/ui-server-auth build ## Deployment -`pnpm -F @proj-airi/ui-server-auth build` writes to `apps/ui-server-auth/dist`. Vue Router owns `/ui/*`, while Vite assets are served from root `/assets/*` so Cloudflare Pages can serve static files without rewriting nested asset paths. `public/_redirects` scopes the SPA rewrite to `/ui/*`, and the top-level `public/404.html` keeps missing assets and other unknown paths as HTTP 404 responses. +`pnpm -F @proj-airi/ui-server-auth build` writes to `apps/ui-server-auth/dist`. Vue Router owns `/ui/*`, while Vite assets are served from root `/assets-v2/*` so Cloudflare Pages can serve static files without rewriting nested asset paths. The versioned namespace moves existing clients away from previously poisoned `/assets/*` browser cache entries. Both namespaces use `Cache-Control: public, no-cache`, allowing browsers to retain files only when Pages revalidates them. `public/_redirects` scopes the SPA rewrite to `/ui/*`, and the top-level `public/404.html` keeps missing assets and other unknown paths as HTTP 404 responses. The production GitHub Actions workflow deploys this app to the Cloudflare Pages project `moeru-ai-airi-auth` with separate auth-account credentials: diff --git a/apps/ui-server-auth/public/_headers b/apps/ui-server-auth/public/_headers index 7f40d0894..5d3ccc5dd 100644 --- a/apps/ui-server-auth/public/_headers +++ b/apps/ui-server-auth/public/_headers @@ -1,4 +1,5 @@ /assets/* - cache-control: max-age=31536000 - cache-control: immutable + cache-control: public, no-cache +/assets-v2/* + cache-control: public, no-cache diff --git a/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts b/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts index d72b3801f..85d967346 100644 --- a/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts +++ b/apps/ui-server-auth/src/cloudflare-pages-routing.test.ts @@ -23,4 +23,12 @@ describe('cloudflare Pages routing', () => { expect(notFoundPage).toContain('Page not found') expect(redirects).toContain('/ui/* / 200') }) + + it('requires old and current asset namespaces to revalidate cached responses', async () => { + const headers = await readFile(resolve(publicDirectory, '_headers'), 'utf8') + + expect(headers).toContain('/assets/*\n cache-control: public, no-cache') + expect(headers).toContain('/assets-v2/*\n cache-control: public, no-cache') + expect(headers).not.toContain('immutable') + }) }) diff --git a/apps/ui-server-auth/vite.config.ts b/apps/ui-server-auth/vite.config.ts index 23afb1ccb..d85d159db 100644 --- a/apps/ui-server-auth/vite.config.ts +++ b/apps/ui-server-auth/vite.config.ts @@ -12,6 +12,16 @@ import VueRouter from 'vue-router/vite' import { defineConfig } from 'vite' +// NOTICE: +// Keep this namespace distinct from `/assets/`, where an earlier Pages SPA +// fallback allowed missing JavaScript URLs to cache index.html as immutable. +// Root cause: the old asset cache policy outlived the deployment that restored +// those files, so affected browsers cannot observe corrected response headers. +// Source/context: `apps/ui-server-auth/public/_headers` and `public/404.html`. +// Removal condition: keep the namespace permanently; reusing `/assets/` can +// reactivate poisoned browser entries that remain fresh for up to one year. +const assetsDirectory = 'assets-v2' + export default defineConfig({ base: '/', optimizeDeps: { @@ -44,6 +54,7 @@ export default defineConfig({ }, }, build: { + assetsDir: assetsDirectory, emptyOutDir: true, manifest: true, outDir: resolve(join(import.meta.dirname, 'dist')), @@ -58,8 +69,8 @@ export default defineConfig({ // Keep analytics as the source-domain name, but explicitly map its // public URL to a neutral chunk name that filter lists cannot infer. return containsAnalyticsModule - ? 'assets/chunk-[hash].js' - : 'assets/[name]-[hash].js' + ? `${assetsDirectory}/chunk-[hash].js` + : `${assetsDirectory}/[name]-[hash].js` }, }, },