Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/adapters/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,31 @@ class NodeServer implements Server {
}
}

serve() {
serve(): Promise<this> {
if (this.#listeningPromise) {
return Promise.resolve(this.#listeningPromise).then(() => this);
return this.#listeningPromise.then(() => this);
}
this.#listeningPromise = new Promise<void>((resolve) => {
this.node!.server!.listen(this.serveOptions, () => {

const server = this.node?.server!;
this.#listeningPromise = new Promise<void>((resolve, reject) => {
const onError = (error: Error) => {
server.off("listening", onListening);
this.#listeningPromise = undefined;
reject(error);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const onListening = () => {
server.off("error", onError);
printListening(this.options, this.url);
resolve();
});
};

server.once("error", onError);
server.once("listening", onListening);
server.listen(this.serveOptions);
});

return this.#listeningPromise.then(() => this);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

get url() {
Expand Down
30 changes: 30 additions & 0 deletions test/fixtures/node-port-conflict/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { serve } from "../../../../src/adapters/node.ts";

const port = Number(process.env.PORT);

process.once("uncaughtException", (error) => {
console.log("uncaught", (error as { code?: string })?.code, error.message);
process.exit(99);
});

const server1 = serve({ port, fetch: () => new Response("one") });
await server1.ready();

try {
const server2 = serve({ port, fetch: () => new Response("two") });
await Promise.race([
server2.ready(),
new Promise((_, reject) => setTimeout(() => reject(new Error("startup timeout")), 500)),
]);

console.log("unexpected-ready");
process.exit(2);
} catch (error) {
console.log(
"caught",
(error as { code?: string })?.code,
error instanceof Error ? error.message : String(error),
);
} finally {
await server1.close(true);
}
23 changes: 23 additions & 0 deletions test/fixtures/node-port-conflict/src/worker-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { WorkerOptions } from "node:worker_threads";

import { Worker } from "node:worker_threads";

const workerBootstrap = /* JavaScript */ `
import { createRequire } from "node:module";
import { workerData } from "node:worker_threads";

const filename = "${import.meta.url}";
const require = createRequire(filename);
const { createJiti } = require("jiti");
const jiti = createJiti(workerData.__ts_worker_filename);

jiti.import(workerData.__ts_worker_filename);
`;

export class TypeScriptWorker extends Worker {
constructor(filename: string | URL, options: WorkerOptions = {}) {
options.workerData ??= {};
options.workerData.__ts_worker_filename = filename.toString();
super(new URL(`data:text/javascript,${workerBootstrap}`), options);
}
}
24 changes: 24 additions & 0 deletions test/fixtures/node-port-conflict/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { parentPort, workerData } from "node:worker_threads";

const { serve } = await import("../../../../src/adapters/node.ts");
const { host, port } = workerData as { host: string; port: number };

const server = serve({
port,
hostname: host,
fetch() {
return new Response("ok");
},
});

try {
await server.ready();
parentPort?.postMessage({ type: "ready" });
} catch (error) {
parentPort?.postMessage({
type: "error",
code: (error as NodeJS.ErrnoException | undefined)?.code,
});
} finally {
await server.close().catch(() => {});
}
69 changes: 68 additions & 1 deletion test/node-adapters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { describe, expect, test } from "vitest";
import { createServer } from "node:http";
import { afterAll, describe, expect, test } from "vitest";
import { getRandomPort } from "get-port-please";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import type { NodeHttp1Handler, NodeServerRequest, NodeServerResponse } from "../src/types.ts";
import { fetchNodeHandler, serve, toNodeHandler, toFetchHandler } from "../src/adapters/node.ts";
import { TypeScriptWorker } from "./fixtures/node-port-conflict/src/worker-helper.ts";

import express from "express";
import fastify from "fastify";
Expand Down Expand Up @@ -257,3 +260,67 @@ describe("request signal", () => {
await server.close(true); // Force close all connections
});
});

const blockerServers = new Set<ReturnType<typeof createServer>>();

afterAll(async () => {
await Promise.all(
[...blockerServers].map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
),
);
});

describe("node server startup", () => {
test("port conflicts reject startup in a worker", async () => {
const port = await getRandomPort("localhost");
const blocker = createServer((_req, res) => res.end("blocked"));
blockerServers.add(blocker);

await new Promise<void>((resolve, reject) => {
blocker.listen(port, "127.0.0.1", (error?: Error) => {
if (error) {
reject(error);
return;
}

resolve();
});
});

const message = await new Promise<{ type: string; code?: string }>((resolve, reject) => {
const worker = new TypeScriptWorker(
new URL("./fixtures/node-port-conflict/src/worker.ts", import.meta.url),
{ workerData: { host: "127.0.0.1", port } },
);

const timeout = setTimeout(() => {
worker.terminate().catch(() => {});
reject(new Error("worker timed out"));
}, 2000);

worker.once("message", (value) => {
clearTimeout(timeout);
void worker.terminate();
resolve(value as { type: string; code?: string });
});

worker.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});

worker.once("exit", (code) => {
if (code !== 0) {
clearTimeout(timeout);
reject(new Error(`worker exited with code ${code}`));
}
});
});

expect(message).toEqual({ type: "error", code: "EADDRINUSE" });
});
});