Skip to content

Editing server api route file causes dev server to hang forever (HMR bug) #4359

Description

@martinszeltins

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 - nuxt/nuxt#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.

  1. Start the dev server and open the app at http://localhost:3000/
  2. Edit layers/common/server/api/user.ts and save it. Everything reloads as normal. ✅
  3. Edit the user.ts file 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:
martinszeltins#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:

Kazam_screencast_00153.mp4

Additional context

No response

Logs

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions