Skip to content

Production server (vinext build + vinext start) returns 404 for every hashed /assets/* file on Windows — cache key built with backslash path #2925

Description

@Lektorx

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 buildvinext 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

  1. Build any vinext/Next-compatible app for production: vinext build.
  2. Start the production server: vinext start -H 127.0.0.1 (or the equivalent host/port you
    use).
  3. 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).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions