From 884571b26ca87011e581ebb83dcac49980cc1d38 Mon Sep 17 00:00:00 2001 From: Artem Samofalov Date: Sun, 27 Sep 2026 14:13:55 +0200 Subject: [PATCH] fix: answer HEAD and */* page requests under vite dev The dev middleware counted only a GET with Accept: text/html as a page request. HEAD, and GETs with */* or no Accept, got Vite's 404 where production renders the page. Use the same test as Vite's HTML fallback. --- .changeset/dev-page-request-head-accept.md | 5 +++ examples/start-ssr/test/run.mjs | 39 ++++++++++++++++++++-- src/ssr/index.ts | 10 ++++-- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 .changeset/dev-page-request-head-accept.md diff --git a/.changeset/dev-page-request-head-accept.md b/.changeset/dev-page-request-head-accept.md new file mode 100644 index 00000000..64c71b04 --- /dev/null +++ b/.changeset/dev-page-request-head-accept.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Under `vite dev`, pages now answer HEAD requests, and GET requests with `Accept: */*` or no Accept, the way production does. Before, these got Vite's 404. The page-request test now matches Vite's own HTML fallback. diff --git a/examples/start-ssr/test/run.mjs b/examples/start-ssr/test/run.mjs index 4880a89b..a02ce364 100644 --- a/examples/start-ssr/test/run.mjs +++ b/examples/start-ssr/test/run.mjs @@ -552,6 +552,33 @@ async function runHttpChecks(mode, origin) { 'post-flush redirect emits the script fallback', post.html.includes('window.location') && post.html.includes('/redirected-target'), ); + + // Pages answer what Vite's HTML fallback counts as navigations, as in + // production: HEAD, a `*/*` GET (curl, fetch()), a GET with no Accept. + const head = await rawRequest(origin + '/', { method: 'HEAD' }); + record( + mode, + 'http', + 'HEAD answers like GET', + head.status === 200 && head.text === '', + `status ${head.status}`, + ); + const anyType = await rawRequest(origin + '/', { headers: { accept: '*/*' } }); + record( + mode, + 'http', + 'GET accepting */* renders the page', + anyType.status === 200 && anyType.text.includes('SSR Start Mode'), + `status ${anyType.status}`, + ); + const noAccept = await rawRequest(origin + '/'); + record( + mode, + 'http', + 'GET without Accept renders the page', + noAccept.status === 200 && noAccept.text.includes('SSR Start Mode'), + `status ${noAccept.status}`, + ); } // Lazy asset-key checks (the /lazy-assets surface, src/App.tsx): a @@ -1969,13 +1996,19 @@ async function runEndpointMode() { functionId ? `got ${JSON.stringify(customText)}` : 'could not extract function id', ); - const fallback = await fetch(`${origin}/_server/${encodeURIComponent(functionId || '')}`); + const fallback = functionId + ? await fetch( + `${origin}/_server/${encodeURIComponent(functionId)}?args=${encodeURIComponent('["endpoint"]')}`, + { method: 'POST' }, + ) + : null; + const fallbackText = fallback ? await fallback.text() : ''; record( mode, 'rpc', 'default endpoint no longer handled', - fallback.status !== 200, - `status ${fallback.status}`, + !!fallback && fallbackText !== 'hello endpoint from the server', + fallback ? `status ${fallback.status}` : 'could not extract function id', ); } catch (e) { record( diff --git a/src/ssr/index.ts b/src/ssr/index.ts index edd419bb..8fb6bb86 100644 --- a/src/ssr/index.ts +++ b/src/ssr/index.ts @@ -1949,8 +1949,14 @@ export function startServe( server.middlewares.use((req, res, next) => { const url = new URL(req.url || '/', 'http://localhost'); if (url.pathname.startsWith('/@')) return next(); - const accept = req.headers.accept || ''; - const pageRequest = req.method === 'GET' && accept.includes('text/html'); + // Vite's own HTML-fallback test: a GET or HEAD accepting HTML by + // name, via `*/*`, or with no Accept (curl, monitors). Production + // renders all of these; anything narrower 404s in dev only. + const accept = req.headers.accept; + const pageRequest = + (req.method === 'GET' || req.method === 'HEAD') && + url.pathname !== '/favicon.ico' && + (!accept || accept.includes('text/html') || accept.includes('*/*')); // Production dispatches every request through the handler, so // dev must too or API routes and no-JS form POSTs served by // `start.middleware` are unreachable under `vite dev`. Without