From 9f1eabb9902ade2f83e73b4bbbc1cb3e8832064a Mon Sep 17 00:00:00 2001 From: Alaister Young <10985857+alaister@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:43:55 +0800 Subject: [PATCH] fix(studio): 404 missing assets instead of serving the SPA shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vercel SPA catch-all rewrite (`${prefix}/(.*)` -> /_shell) swallowed missing extensioned paths, so a stale hashed chunk (e.g. requested after a redeploy) returned text/html 200 instead of 404. The browser then threw 'Failed to load module script ... MIME type text/html', and because /assets/* is cached immutable the bogus HTML poisoned the edge cache under the asset URL (defeating skew protection, which is served after the cache). Make the shell rewrite match only extensionless paths via a negative lookahead so a missing file with an extension 404s cleanly — letting skew protection's __vdpl routing serve the chunk from the deployment that still has it, or the client's vite:preloadError backstop recover. --- apps/studio/vercel.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/studio/vercel.ts b/apps/studio/vercel.ts index f5e5f6d5458..2c0275a63a0 100644 --- a/apps/studio/vercel.ts +++ b/apps/studio/vercel.ts @@ -32,16 +32,27 @@ const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '' // // Rewrite ordering: API + server-function passthrough first so extensioned // API paths (/api/foo.json) don't get caught by the asset rule. Asset rule -// next — it's an identity rewrite that also guards missing files from -// falling through to the shell (a missing .js should 404, not serve HTML). -// Shell rule last, catching everything else. +// next — strips the basePath prefix so `/dashboard/assets/x.js` maps onto the +// `dist/client/assets/x.js` filesystem layout (a no-op identity when +// prefix=''). Shell rule LAST, and it deliberately matches only extensionless +// paths via a negative lookahead. +// +// Why the lookahead matters: a request WITH a file extension that doesn't +// resolve to a real file — e.g. a hashed chunk from an older deployment after +// a redeploy — must fall through to a 404, NOT the HTML shell. A catch-all +// `(.*)` shell swallows those misses and returns `text/html`, so the browser +// gets HTML for a `.js` request and throws "Failed to load module script … +// MIME type text/html" (and, because /assets/* is cached immutable, that HTML +// poisons the edge cache under the asset URL). A clean 404 instead lets skew +// protection's `__vdpl` routing serve the chunk from the deployment that still +// has it, or the client's `vite:preloadError` backstop recover. function routesFor(prefix: string) { return { rewrites: [ routes.rewrite(`${prefix}/api/(.*)`, '/api/server'), routes.rewrite(`${prefix}/_serverFn/(.*)`, '/api/server'), routes.rewrite(`${prefix}/(.*\\.\\w+)`, '/$1'), - routes.rewrite(`${prefix}/(.*)`, '/_shell'), + routes.rewrite(`${prefix}/((?!.*\\.\\w+$).*)`, '/_shell'), ], headers: [ // Dynamic function responses must not be cached by any shared cache —