vercel / vercel/next.js

Dev server reads its emitted manifests while they are zero bytes, failing in-flight requests during a rebuild

Open
#97,594 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/Faolain/nextjs-dev-manifest-empty-race

To Reproduce
npm install
npm run repro     # boots the dev server, then hammers it for 90s

The script keeps requests to 12 App Router routes in flight continuously while
appending to a client component to force rebuilds underneath them. It prints
REPRODUCED and exits 0 as soon as the error lands in dev.log, typically
within the first 30s.

Both ingredients are required: concurrent requests and a rebuild while they
are in flight. Requesting one route at a time will not reproduce it.

Current vs. Expected behavior

Current: a request that overlaps a rebuild can read one of the bundler's
emitted manifests while the file is zero bytes, and the request fails:

⨯ Error: Manifest file is empty
    at ignore-listed frames { page: '/pricing' }
 GET /pricing 500 in 1036ms

Expected: a rebuild does not make in-flight requests fail.

Analysis

In dev, RouteModule loads its manifests with shouldCache: !this.isDev, so
every request re-reads them from disk rather than reading a cached copy. The
bundler rewrites those same files on every rebuild. A request landing between a
manifest being created and its contents being written reads a zero-length file.

Which error you get depends only on which loader lost the race:

manifest loader symptom
.next/dev/server/app/<page>_client-reference-manifest.js evalManifest Error: Manifest file is empty (E328)
.next/dev/server/next-font-manifest.json loadManifest SyntaxError: Unexpected end of JSON input
.next/dev/build-manifest.json loadManifest SyntaxError: Unexpected end of JSON input

Two measurements that I think matter for choosing a fix:

  1. The file is always exactly zero bytes, never partially-written garbage. I
    instrumented loadManifest to log path and content.length on parse
    failure; every hit was len=0.

  2. It stays zero bytes for a sustained window, so this is not a torn write. I
    instrumented both loaders to re-read up to 5 times over ~10ms before giving
    up. Every retry exhausted all 5 attempts with the file still at len=0. A
    short retry inside the read does not fix this.

handleMissing does not help today: it catches the readFileSync throw for a
file that is absent, but a file that exists and is empty does not throw, so it
falls through to JSON.parse('') or to the E328 throw.

Possible fixes

The root fix is presumably that these manifests should reach their final path
atomically (write + rename), so a reader can never observe the empty window.
That matches the direction of #96384 / #96695, which use write-file-atomic for
prerender-manifest.json — though those are Next's own writes, whereas the three
manifests above are emitted as bundler assets, so the write path is different.

As a narrower mitigation I opened https://github.com/vercel/next.js/pull/97593, which makes evalManifest treat a
zero-length read the same way it already treats an absent file when the caller
passed handleMissing (that call site already optional-chains the result). In
the repro that removes the E328 failures completely. It deliberately does not
address the two JSON manifests, because those call sites do not pass
handleMissing and their consumers are not typed for undefined — so I did not
want to change their contract without a maintainer's read on it.

Notes

Verified on next@16.3.0 (webpack), macOS 15, Node 22.15. 16.3.1 and current
canary contain the identical loader — I diffed the published
dist/server/load-manifest.external.js and the canary source, and neither has
any handling for an empty read.

This was originally filed as #96971, which the bot auto-closed for an invalid
reproduction link. This issue carries a working public repro.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the linked reproduction with npm install and npm run repro, then inspect RouteModule's manifest-loading calls and the evalManifest and loadManifest paths. Compare the emitted manifest paths and the existing PR's mitigation; done means concurrent requests remain successful during rebuilds without exposing empty manifest reads.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, node.js, webpack
Domain
backend, build-system
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.