Skip to content

Commit 0e4f29b

Browse files
nekomeowwwpi0
andauthored
fix(node): handle EADDRINUSE port conflict on serve (#197)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
1 parent 645b261 commit 0e4f29b

2 files changed

Lines changed: 75 additions & 6 deletions

File tree

src/adapters/node.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class NodeServer implements Server {
5252
readonly #isSecure?: boolean;
5353

5454
#listeningPromise?: Promise<void>;
55+
#listenError?: Error;
5556

5657
#wait?: ReturnType<typeof createWaitUntil>;
5758

@@ -120,20 +121,41 @@ class NodeServer implements Server {
120121
this.node.server = server;
121122

122123
if (!options.manual) {
123-
this.serve();
124+
this.serve().catch(() => {});
124125
}
125126
}
126127

127-
serve() {
128+
serve(): Promise<this> {
128129
if (this.#listeningPromise) {
129-
return Promise.resolve(this.#listeningPromise).then(() => this);
130+
return this.#listeningPromise.then(() => this);
130131
}
131-
this.#listeningPromise = new Promise<void>((resolve) => {
132-
this.node!.server!.listen(this.serveOptions, () => {
132+
133+
const server = this.node?.server;
134+
if (!server) {
135+
return Promise.reject(new Error("Server not initialized"));
136+
}
137+
138+
this.#listenError = undefined;
139+
this.#listeningPromise = new Promise<void>((resolve, reject) => {
140+
const onError = (error: Error) => {
141+
server.off("listening", onListening);
142+
this.#listenError = error;
143+
this.#listeningPromise = undefined;
144+
reject(error);
145+
};
146+
147+
const onListening = () => {
148+
server.off("error", onError);
133149
printListening(this.options, this.url);
134150
resolve();
135-
});
151+
};
152+
153+
server.once("error", onError);
154+
server.once("listening", onListening);
155+
server.listen(this.serveOptions);
136156
});
157+
158+
return this.#listeningPromise.then(() => this);
137159
}
138160

139161
get url() {
@@ -148,6 +170,9 @@ class NodeServer implements Server {
148170
}
149171

150172
ready(): Promise<Server> {
173+
if (this.#listenError) {
174+
return Promise.reject(this.#listenError);
175+
}
151176
return Promise.resolve(this.#listeningPromise).then(() => this);
152177
}
153178

test/node-adapters.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createServer } from "node:http";
2+
import type { AddressInfo } from "node:net";
13
import { describe, expect, test } from "vitest";
24

35
import type { NodeHttp1Handler, NodeServerRequest, NodeServerResponse } from "../src/types.ts";
@@ -257,3 +259,45 @@ describe("request signal", () => {
257259
await server.close(true); // Force close all connections
258260
});
259261
});
262+
263+
describe("node server startup", () => {
264+
async function withBlockedPort(fn: (port: number) => Promise<void>) {
265+
const blocker = createServer((_req, res) => res.end("blocked"));
266+
await new Promise<void>((resolve, reject) => {
267+
blocker.once("error", reject);
268+
blocker.once("listening", () => resolve());
269+
blocker.listen(0, "127.0.0.1");
270+
});
271+
const { port } = blocker.address() as AddressInfo;
272+
try {
273+
await fn(port);
274+
} finally {
275+
await new Promise<void>((resolve) => blocker.close(() => resolve()));
276+
}
277+
}
278+
279+
test("port conflict rejects with EADDRINUSE", async () => {
280+
await withBlockedPort(async (port) => {
281+
const server = serve({
282+
port,
283+
hostname: "127.0.0.1",
284+
manual: true,
285+
fetch: () => new Response(""),
286+
});
287+
await expect(server.serve()).rejects.toMatchObject({ code: "EADDRINUSE" });
288+
await server.close();
289+
});
290+
});
291+
292+
test("auto-serve port conflict surfaces via ready()", async () => {
293+
await withBlockedPort(async (port) => {
294+
const server = serve({
295+
port,
296+
hostname: "127.0.0.1",
297+
fetch: () => new Response(""),
298+
});
299+
await expect(server.ready()).rejects.toMatchObject({ code: "EADDRINUSE" });
300+
await server.close();
301+
});
302+
});
303+
});

0 commit comments

Comments
 (0)