next.js
ceff7268 - Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable (#93877)

Commit
47 days ago
Turbopack: don't crash the dev server when node_modules/next is briefly unresolvable (#93877) ### What? When `node_modules/next` is briefly unresolvable mid-session (for example because pnpm is mid-install of a package with a `next` peer dependency, reshuffling its symlinks) Turbopack would: - Emit a `MissingNextFolderIssue` with severity `Fatal` - Propagate `anyhow!("Next.js package not found")` through ~10 levels of the build graph - Surface the failure at the napi boundary as a `TurbopackInternalError`, which the Next.js JS-side treats as fatal and shuts the dev server down The reported user impact was a dev session that became wedged and could not recover even after restarting `next dev`, because the persistent cache had stored the failed operation state. Users reported it as "stuck", "catastrophic", and required wiping `.next/` to recover. Unfortunately while i was able to reproduce a number of bad outcomes, i couldn't actually reproduce the issue of `next dev` getting stuck in a bad state. Still this PR will improve a number of apis and error messages See internal discussion: https://vercel.slack.com/archives/C046HAU4H7F/p1778792876550309 ### Why? A transient filesystem race during an install is the canonical recoverable error — once the install finishes, `next` is resolvable again. There is no reason this should bring down the dev server or poison the persistent cache. The bug had two contributing causes: 1. The error message was poor. It assumed the only cause was a mis-set `turbopack.root`, with the entire message in `title()`. It didn't acknowledge the install-race case or any of the other realistic causes (broken symlink, monorepo hoisting, removed `node_modules/next`, global install). Users had no way to know that the right response was to wait or refresh. 2. The Rust→napi boundary did not treat this as a recoverable error. Two specific code paths propagated `Err` instead of catching it: - `issue_filter_from_endpoint` in `endpoint.rs` calls `endpoint_op.connect().await?` — if the upstream endpoint resolution errors (as it does when `directory_tree_to_loader_tree` fails), this `?` propagates **before** the surrounding `strongly_consistent_catch_collectables` gets a chance to convert the failure into Issues. - `Project::hmr_version_state` is called bare from `project_hmr_events` at the napi boundary. There is no `_with_issues_operation` wrapper, so any error from the underlying `hmr_content`/`versioned_content_map.compute_entry` chain propagates straight to JS. ### How? Three changes: **1. Rewrite `MissingNextFolderIssue`** ([next_import_map.rs](crates/next-core/src/next_import_map.rs)) - Severity downgraded from `Fatal` to `Error`. `Fatal` triggers an explicit JS-side server shutdown in `print-build-errors.ts`; `Error` lets the dev server recover. - Title is a single line: `"Could not find the Next.js package (next/package.json)"`. - Description lists all realistic causes (concurrent install, missing/broken symlink, mis-set workspace root, monorepo hoisting, global install). - Doc link moved to `documentation_link()` so it doesn't pollute the inline message. **2. Make `issue_filter_from_endpoint` Err-tolerant** ([endpoint.rs](crates/next-napi-bindings/src/next_api/endpoint.rs)) - Wrap the `endpoint_op.connect().await` in a `match` and fall back to `IssueFilter::warnings_and_foreign_errors()` on `Err`. - This unblocks the existing `strongly_consistent_catch_collectables` recovery path, which then absorbs the upstream failure as Issues that surface through the napi boundary normally. **3. Add `hmr_version_state_with_issues_operation`** ([project.rs](crates/next-napi-bindings/src/next_api/project.rs)) - Mirrors the existing `hmr_update_with_issues_operation` pattern. - Drives `hmr_version_operation` as an `OperationVc`, catches any `Err` and falls back to `NotFoundVersion`, and collects issues from the operation via `peek_issues`. - The napi `project_hmr_events` handler now uses this wrapper instead of calling `Project::hmr_version_state` bare. Issues from both `hmr_version_state` and `hmr_update` are merged before being sent to JS. - Required lifting the inner `hmr_version_operation` out of `Project::hmr_version_state`'s closure to make it callable from the napi layer. A new regression test at [test/development/app-dir/concurrent-install/](test/development/app-dir/concurrent-install/) reproduces the failure by moving `node_modules/next` aside mid-HMR-session and asserts that the dev server does not emit `FATAL: An unexpected Turbopack error occurred` or `TurbopackInternalError`, and that the friendly Issue text surfaces. The test is skipped under `NEXT_SKIP_ISOLATE=1` since that mode doesn't produce a manipulable `node_modules`. Out of scope: after recovery, the dev server still returns 500 on subsequent requests until the user manually refreshes (the JS-side per-route error state isn't invalidated by Turbopack's successful recompile). That's a separate dev-server caching issue. <!-- NEXT_JS_LLM_PR -->
Author
Parents
Loading