Environment
- OS: Windows 11 (reproduced on Windows 11 Home 10.0.26200)
vinext: 0.0.50
vite: 8.0.13
- Node:
>=22 (per vinext engines field)
- Reproduced with a standard
vinext build → vinext start -H 127.0.0.1 flow (no custom
server code, no Cloudflare Workers runtime involved — plain Node static file serving)
Summary
After vinext build followed by vinext start, every request for a hashed static asset under
/assets/* (JS/CSS bundles emitted by the build with content-hash filenames, e.g.
/assets/index-<hash>.js) returns HTTP 404, even though the file physically exists in the
built dist/client/assets/ output directory. Pages that reference these assets load with
HTTP 200 for the HTML/RSC response itself, but every hashed asset request inside that page
fails to load. The dev server (vinext dev) is not affected — only the production
build+start path.
Steps to reproduce
- Build any vinext/Next-compatible app for production:
vinext build.
- Start the production server:
vinext start -H 127.0.0.1 (or the equivalent host/port you
use).
- Request any page that references a hashed asset from
dist/client/assets/, e.g. via
curl -v http://127.0.0.1:<port>/assets/<hashed-file>.js directly (bypassing the browser
entirely reproduces it too — this is a plain HTTP static-file lookup, not a Playwright/
browser-specific artifact).
- Observe: HTTP 404, even though the file is present on disk under
dist/client/assets/<hashed-file>.js.
This was independently confirmed three ways in our testing: (a) via a real browser loading a
page, (b) via plain curl -v against the asset URL directly (response carries RSC-related
headers, no static-file handler match), and (c) via a direct unit-level call into vinext's own
static file cache: StaticFileCache.create(clientDir).lookup("/assets/index-<hash>.js")
returns undefined/false even though fs confirms the file exists at that path.
Expected behavior
Hashed assets under /assets/* are served with HTTP 200 by the production server, identical to
how they are served by vinext dev.
Actual behavior
Every hashed /assets/* request returns HTTP 404 in production (vinext build + vinext start) on Windows. The app is effectively unusable in a production build on Windows, because
no client-side bundle can load.
Root cause hypothesis, with code location
node_modules/vinext/dist/server/static-file-cache.js, function walkFilesWithStats
(around line 190–214 in 0.0.50), builds each cache entry's key from a directory walk:
async function* walkFilesWithStats(dir, base = dir) {
...
for (let j = 0; j < batch.length; j++) yield {
relativePath: path.relative(base, batch[j]), // <-- line ~207
fullPath: batch[j],
stat: { size: stats[j].size, mtimeMs: stats[j].mtimeMs },
};
}
path is imported as the default node:path module (not node:path/posix), so on Windows
path.relative(base, batch[j]) returns a path with backslash separators (e.g.
assets\index-<hash>.js). This relativePath is then used directly, unmodified, to build the
lookup key in StaticFileCache.create():
const pathname = "/" + relativePath; // becomes "/assets\index-<hash>.js" on Windows
entries.set(pathname, entry);
At request time, StaticFileCache.lookup(pathname) (same file, lookup() method) is called
with the pathname parsed from the incoming HTTP request URL, which always uses forward
slashes (e.g. /assets/index-<hash>.js, per URL/HTTP convention — browsers and curl never
send backslash paths). Because Map.get() does an exact string match and
/assets\index-<hash>.js !== /assets/index-<hash>.js, the lookup always misses on Windows,
regardless of the file genuinely existing on disk. On POSIX (Linux/macOS), path.relative
already returns forward-slash paths, so the same code path does not exhibit this bug there —
consistent with this being platform-specific to Windows.
Suggested fix direction (not verified against the full codebase, offered only as a
hypothesis): normalize relativePath to forward slashes immediately after
path.relative(...) in walkFilesWithStats (e.g. relativePath.split(path.sep).join("/"), or
use path.posix semantics for the derived pathname), so the cache key always matches the
forward-slash pathname used both by incoming request URLs and by the entries.set(pathname, entry) call a few lines below in StaticFileCache.create().
Workaround we are using
None needed for production correctness assessment, but for our own CI/tooling we avoid the
broken path entirely by using vinext dev -H 127.0.0.1 (the standard dev server, equivalent to
npm run dev) instead of vinext build + vinext start wherever we need a running instance on
Windows. This is a workaround for our usage, not a fix — vinext start production serving
remains broken on Windows as far as we can tell.
Additional notes
- We have not patched
node_modules/vinext ourselves and are not proposing to; this report is
the intended fix path.
- Happy to provide a minimal repro repository if useful — the app used to find this was a
standard Next-compatible app with default vite build-style hashed asset filenames, nothing
exotic in the routing or asset pipeline.
Environment
vinext:0.0.50vite:8.0.13>=22(pervinextenginesfield)vinext build→vinext start -H 127.0.0.1flow (no customserver code, no Cloudflare Workers runtime involved — plain Node static file serving)
Summary
After
vinext buildfollowed byvinext start, every request for a hashed static asset under/assets/*(JS/CSS bundles emitted by the build with content-hash filenames, e.g./assets/index-<hash>.js) returns HTTP 404, even though the file physically exists in thebuilt
dist/client/assets/output directory. Pages that reference these assets load withHTTP 200 for the HTML/RSC response itself, but every hashed asset request inside that page
fails to load. The dev server (
vinext dev) is not affected — only the productionbuild+start path.
Steps to reproduce
vinext build.vinext start -H 127.0.0.1(or the equivalent host/port youuse).
dist/client/assets/, e.g. viacurl -v http://127.0.0.1:<port>/assets/<hashed-file>.jsdirectly (bypassing the browserentirely reproduces it too — this is a plain HTTP static-file lookup, not a Playwright/
browser-specific artifact).
dist/client/assets/<hashed-file>.js.This was independently confirmed three ways in our testing: (a) via a real browser loading a
page, (b) via plain
curl -vagainst the asset URL directly (response carries RSC-relatedheaders, no static-file handler match), and (c) via a direct unit-level call into vinext's own
static file cache:
StaticFileCache.create(clientDir).lookup("/assets/index-<hash>.js")returns
undefined/falseeven thoughfsconfirms the file exists at that path.Expected behavior
Hashed assets under
/assets/*are served with HTTP 200 by the production server, identical tohow they are served by
vinext dev.Actual behavior
Every hashed
/assets/*request returns HTTP 404 in production (vinext build+vinext start) on Windows. The app is effectively unusable in a production build on Windows, becauseno client-side bundle can load.
Root cause hypothesis, with code location
node_modules/vinext/dist/server/static-file-cache.js, functionwalkFilesWithStats(around line 190–214 in
0.0.50), builds each cache entry's key from a directory walk:pathis imported as the defaultnode:pathmodule (notnode:path/posix), so on Windowspath.relative(base, batch[j])returns a path with backslash separators (e.g.assets\index-<hash>.js). ThisrelativePathis then used directly, unmodified, to build thelookup key in
StaticFileCache.create():At request time,
StaticFileCache.lookup(pathname)(same file,lookup()method) is calledwith the pathname parsed from the incoming HTTP request URL, which always uses forward
slashes (e.g.
/assets/index-<hash>.js, per URL/HTTP convention — browsers andcurlneversend backslash paths). Because
Map.get()does an exact string match and/assets\index-<hash>.js!==/assets/index-<hash>.js, the lookup always misses on Windows,regardless of the file genuinely existing on disk. On POSIX (Linux/macOS),
path.relativealready returns forward-slash paths, so the same code path does not exhibit this bug there —
consistent with this being platform-specific to Windows.
Suggested fix direction (not verified against the full codebase, offered only as a
hypothesis): normalize
relativePathto forward slashes immediately afterpath.relative(...)inwalkFilesWithStats(e.g.relativePath.split(path.sep).join("/"), oruse
path.posixsemantics for the derived pathname), so the cache key always matches theforward-slash pathname used both by incoming request URLs and by the
entries.set(pathname, entry)call a few lines below inStaticFileCache.create().Workaround we are using
None needed for production correctness assessment, but for our own CI/tooling we avoid the
broken path entirely by using
vinext dev -H 127.0.0.1(the standard dev server, equivalent tonpm run dev) instead ofvinext build+vinext startwherever we need a running instance onWindows. This is a workaround for our usage, not a fix —
vinext startproduction servingremains broken on Windows as far as we can tell.
Additional notes
node_modules/vinextourselves and are not proposing to; this report isthe intended fix path.
standard Next-compatible app with default
vite build-style hashed asset filenames, nothingexotic in the routing or asset pipeline.