Editing server api route file causes dev server to hang forever (HMR bug)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11.2k
- Forks
- 899
- Avg merge
- 2d 24m
- Merged PRs (30d)
- 40
Description
Environment
╭── Nuxt project info (copied to clipboard) ───────────────────────────────────╮
│ │
│ Operating system Ubuntu 20.04 Linux 5.15.0-179-generic │
│ CPU Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz (8 cores) │
│ Node.js version v24.11.0 │
│ nuxt/cli version 3.35.2 │
│ Package manager pnpm 10.31.0 │
│ Nuxt version 4.4.8 │
│ Nitro version 2.13.4 │
│ Builder vite 7.3.5 │
│ Config app, compatibilityDate, devtools, modules │
│ Modules - │
╰──────────────────────────────────────────────────────────────────────────────╯
Reproduction
https://stackblitz.com/github/martinszeltins/nuxt-hmr-hang-bug
Describe the bug
Initially reported to Nuxt here - https://github.com/nuxt/nuxt/issues/35374, there is a bug that causes HMR to hang forever in loading state.
On first edit, it reloads and everything seems to be normal. Then on second edit + file save - the dev server totally hangs forever. I have attached a screencast to easily see the problem.
- Start the dev server and open the app at http://localhost:3000/
- Edit
layers/common/server/api/user.tsand save it. Everything reloads as normal. ✅ - Edit the
user.tsfile one more time and save it. The dev server now hangs forever ❌ 😔
With the help of AI, I was able to get a fix and added failing tests:
In Nitropack’s DevServer, during HMR it was routing requests to a stale worker while building=true, then killing that worker on reload — leaving SSR requests hung.
During Nitro HMR, getWorker() could still hand requests to an old dev worker while building=true, and dev:reload then killed that worker mid-SSR — so the browser reload never finished.
The patch closes workers as soon as a rebuild starts and makes getWorker() wait for a fresh worker instead of reusing a stale one during building, with a 120s safety cap.
Here is a repo with the patch applied that seems to fix the issue:
https://github.com/martinszeltins/nuxt-nitro-hang-patch-fix
pnpm-workspace.yaml:
patchedDependencies:
nitropack@2.13.4: patches/nitropack@2.13.4.patch
patches/nitropack@2.13.4.patch:
diff --git a/dist/core/index.mjs b/dist/core/index.mjs
index 8a601e0360c576d1e1c0e73da413d2393b8beb26..757aeea4e03af9a2180d9ec4f9239f9e31c104a1 100644
--- a/dist/core/index.mjs
+++ b/dist/core/index.mjs
@@ -2682,6 +2682,9 @@ class DevServer {
nitro.hooks.hook("dev:start", () => {
this.building = true;
this.buildError = void 0;
+ for (const worker of this.workers) {
+ worker.close();
+ }
});
nitro.hooks.hook("dev:reload", () => {
this.buildError = void 0;
@@ -2755,12 +2758,17 @@ class DevServer {
async getWorker() {
let retry = 0;
const maxRetries = isTest || isCI ? 100 : 10;
+ const getWorkerStart = Date.now();
+ const maxBuildingWaitMs = 120_000;
while (this.building || ++retry < maxRetries) {
+ if (Date.now() - getWorkerStart > maxBuildingWaitMs) {
+ return;
+ }
if ((this.workers.length === 0 || this.buildError) && !this.building) {
return;
}
const activeWorker = this.workers.find((w) => w.ready);
- if (activeWorker) {
+ if (activeWorker && !this.building) {
return activeWorker;
}
await new Promise((resolve2) => setTimeout(resolve2, 600));
HMR tests failing tests that show the hang bug:
https://github.com/martinszeltins/nitro/pull/1
Patch that makes the tests pass:
diff --git a/src/core/dev-server/server.ts b/src/core/dev-server/server.ts
index 3d46c8ac..7da6df9d 100644
--- a/src/core/dev-server/server.ts
+++ b/src/core/dev-server/server.ts
@@ -85,6 +85,9 @@ class DevServer {
nitro.hooks.hook("dev:start", () => {
this.building = true;
this.buildError = undefined;
+ for (const worker of this.workers) {
+ worker.close();
+ }
});
nitro.hooks.hook("dev:reload", () => {
@@ -164,12 +167,17 @@ class DevServer {
async getWorker() {
let retry = 0;
const maxRetries = isTest || isCI ? 100 : 10;
+ const getWorkerStart = Date.now();
+ const maxBuildingWaitMs = 120_000;
while (this.building || ++retry < maxRetries) {
+ if (Date.now() - getWorkerStart > maxBuildingWaitMs) {
+ return;
+ }
if ((this.workers.length === 0 || this.buildError) && !this.building) {
return;
}
const activeWorker = this.workers.find((w) => w.ready);
- if (activeWorker) {
+ if (activeWorker && !this.building) {
return activeWorker;
}
await new Promise((resolve) => setTimeout(resolve, 600));
Screencast:
https://github.com/user-attachments/assets/966a7b43-fb47-416a-8d1a-babed415aa4d
Additional context
No response
Logs
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
Start by reproducing the two-save hang with the linked StackBlitz project, then inspect src/core/dev-server/server.ts and the HMR tests linked in the report. Trace the dev:start, dev:reload, and getWorker flow, and verify that repeated edits to layers/common/server/api/user.ts complete normally and the failing HMR tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript, vite
- Domain
- backend, developer-experience, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100