diff --git a/.changeset/fix-close-websocket-servers-dispose.md b/.changeset/fix-close-websocket-servers-dispose.md new file mode 100644 index 0000000000..e0722305c4 --- /dev/null +++ b/.changeset/fix-close-websocket-servers-dispose.md @@ -0,0 +1,7 @@ +--- +"miniflare": patch +--- + +Close WebSocket servers during dispose to prevent lingering connections + +The `#liveReloadServer` and `#webSocketServer` (both created with `noServer: true`) were never explicitly closed during `Miniflare.dispose()`. Connected WebSocket clients (e.g., browser live reload, devtools) would keep sockets alive, preventing the Node.js event loop from draining cleanly. diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 20b433349b..a218148bfc 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -3030,6 +3030,17 @@ export class Miniflare { await this.#runtime?.dispose(); await this.#stopLoopbackServer(); + // Terminate connected WebSocket clients and close the WebSocket servers. + // These are created with `noServer: true` so they aren't closed by + // stopping the loopback HTTP server. + for (const ws of this.#liveReloadServer.clients) { + ws.terminate(); + } + this.#liveReloadServer.close(); + for (const ws of this.#webSocketServer.clients) { + ws.terminate(); + } + this.#webSocketServer.close(); // Best-effort cleanup: on Windows, workerd may not release file handles // immediately after disposal, causing EBUSY errors. The temp directory // lives in os.tmpdir() so the OS will clean it up eventually.