Dev server reads its emitted manifests while they are zero bytes, failing in-flight requests during a rebuild
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:
-
The file is always exactly zero bytes, never partially-written garbage. I
instrumentedloadManifestto logpathandcontent.lengthon parse
failure; every hit waslen=0. -
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 atlen=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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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