What versions & operating system are you using?
miniflare 5.20260801.1-alpha (reproduced against main @ 6dbd192), Node v24.14.1, macOS 15.
Please provide a link to a minimal reproduction
Inline below — six lines, no config or account needed.
Describe the Bug
Calling Miniflare#dispose() a second time rejects with ERR_SERVER_NOT_RUNNING ("Server is not running.").
dispose() has no "already disposed" guard, so a second call re-runs the whole teardown sequence. Two of those steps close a Node HTTP server that the first call already closed, and both propagate the resulting error:
#stopLoopbackServer() (packages/miniflare/src/index.ts):
#stopLoopbackServer(): Promise<void> {
return new Promise((resolve, reject) => {
assert(this.#loopbackServer !== undefined);
this.#loopbackServer.stop((err) => (err ? reject(err) : resolve()));
});
}
InspectorProxyController#dispose() (.../inspector-proxy/inspector-proxy-controller.ts):
server.closeAllConnections();
return new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
server.close() calls back with ERR_SERVER_NOT_RUNNING when the server was not listening. Because the rejection escapes dispose() mid-way, every cleanup step after the failing one is skipped. await this.#stopLoopbackServer() sits early in the finally block, so a second dispose() abandons all of:
await this.#stopLoopbackServer(); // ← rejects here
this.#webSocketServer.close();
removeDir(this.#tmpPath, { fireAndForget: true });
// ... email session dirs ...
await this.#maybeInspectorProxyController?.dispose(); // ← would reject here too
await this.#devRegistry.dispose(); // unregister + stop file watcher
await this.#hyperdriveProxyController.dispose();
maybeInstanceRegistry?.delete(this);
So the dev registry file watcher and its registration files, the Hyperdrive proxy servers, and the instance registry entry are all left behind — which is the opposite of what a caller retrying dispose() is trying to achieve.
This is already being worked around inside this repo. packages/miniflare/test/test-shared/miniflare.ts string-matches the message to keep test teardown green:
} catch (e) {
lastError = e;
// Treat "already disposed" as success (idempotent disposal)
const message = (e as Error).message;
if (message === "Server is not running.") {
return;
}
Steps to reproduce
import { Miniflare } from "miniflare";
const mf = new Miniflare({ script: "", modules: true });
await mf.ready;
await mf.dispose();
await mf.dispose(); // ← rejects
Actual
Error [ERR_SERVER_NOT_RUNNING]: Server is not running.
at Server.close (node:net:2367:12)
at Object.onceWrapper (node:events:622:28)
at Server.emit (node:events:508:28)
at emitCloseNT (node:net:2427:8)
at processTicksAndRejections (node:internal/process/task_queues:89:21)
The same happens with an inspector proxy running (inspectorPort: 0 + unsafeInspectorProxy: true) — and there the inspector proxy server is a second independent source of the same rejection, so fixing only the loopback server still leaves that path broken.
Expected
dispose() is idempotent: a second call resolves and the remaining cleanup still runs. Closing an already-closed server is not a failure to close it.
Please provide any relevant error logs
See the stack above. The failure is silent about what it skipped — the leftover dev registry watcher just keeps the Node event loop alive.
What versions & operating system are you using?
miniflare5.20260801.1-alpha (reproduced againstmain@ 6dbd192), Node v24.14.1, macOS 15.Please provide a link to a minimal reproduction
Inline below — six lines, no config or account needed.
Describe the Bug
Calling
Miniflare#dispose()a second time rejects withERR_SERVER_NOT_RUNNING("Server is not running.").dispose()has no "already disposed" guard, so a second call re-runs the whole teardown sequence. Two of those steps close a Node HTTP server that the first call already closed, and both propagate the resulting error:#stopLoopbackServer()(packages/miniflare/src/index.ts):InspectorProxyController#dispose()(.../inspector-proxy/inspector-proxy-controller.ts):server.close()calls back withERR_SERVER_NOT_RUNNINGwhen the server was not listening. Because the rejection escapesdispose()mid-way, every cleanup step after the failing one is skipped.await this.#stopLoopbackServer()sits early in thefinallyblock, so a seconddispose()abandons all of:So the dev registry file watcher and its registration files, the Hyperdrive proxy servers, and the instance registry entry are all left behind — which is the opposite of what a caller retrying
dispose()is trying to achieve.This is already being worked around inside this repo.
packages/miniflare/test/test-shared/miniflare.tsstring-matches the message to keep test teardown green:Steps to reproduce
Actual
The same happens with an inspector proxy running (
inspectorPort: 0+unsafeInspectorProxy: true) — and there the inspector proxy server is a second independent source of the same rejection, so fixing only the loopback server still leaves that path broken.Expected
dispose()is idempotent: a second call resolves and the remaining cleanup still runs. Closing an already-closed server is not a failure to close it.Please provide any relevant error logs
See the stack above. The failure is silent about what it skipped — the leftover dev registry watcher just keeps the Node event loop alive.