diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..83beb31
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Set default behavior to automatically convert line endings
+* text=auto eol=lf
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..c909645
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,49 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ commit-message:
+ include: "scope"
+ prefix: "ci"
+ schedule:
+ interval: "monthly"
+ cooldown:
+ default-days: 7
+ allow:
+ - dependency-name: "*"
+ update-types:
+ - "version-update:semver-major"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: "npm"
+ directory: "/"
+ commit-message:
+ include: "scope"
+ prefix: "build"
+ schedule:
+ interval: "monthly"
+ cooldown:
+ default-days: 7
+ versioning-strategy: "increase-if-necessary"
+ allow:
+ - dependency-name: "*"
+ update-types:
+ - "version-update:semver-major"
+ open-pull-requests-limit: 10
+ groups:
+ # Production dependencies with breaking changes
+ dependencies:
+ dependency-type: "production"
+ patterns:
+ - "*"
+ # TypeScript related dependencies
+ dev-dependencies-typescript:
+ patterns:
+ - "@types/*"
+ - "tstyche"
+ - "typescript"
+ # Development dependencies with breaking changes
+ dev-dependencies:
+ dependency-type: "development"
+ patterns:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..dd784e5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - next
+ - "v*"
+ paths-ignore:
+ - "docs/**"
+ - "*.md"
+ pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "*.md"
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ permissions:
+ contents: write
+ pull-requests: write
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
+ with:
+ node-versions: '["24", "26"]'
+ license-check: true
+ lint: true
diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml
new file mode 100644
index 0000000..ca4f713
--- /dev/null
+++ b/.github/workflows/lock-threads.yml
@@ -0,0 +1,19 @@
+name: Lock Threads
+
+on:
+ schedule:
+ - cron: "0 0 1 * *"
+ workflow_dispatch:
+
+concurrency:
+ group: lock
+
+permissions:
+ contents: read
+
+jobs:
+ lock-threads:
+ permissions:
+ issues: write
+ pull-requests: write
+ uses: fastify/workflows/.github/workflows/lock-threads.yml@2073dc8e1f9e172bf42daa3843c9dbd31af1e8cb # v6.0.0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7cf7be9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,143 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.*
+!.env.example
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+.output
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp directory
+.temp
+
+# Sveltekit cache directory
+.svelte-kit/
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# Firebase cache directory
+.firebase/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# pnpm
+.pnpm-store
+
+# yarn v3
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+
+# Vite files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+.vite/
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..55e7da5
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+ignore-scripts=true
+min-release-age=7
+package-lock=false
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2ba2498
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# @fastify/server
+
+## Install
+
+```shell
+npm install @fastify/server
+```
+
+## Example
+
+This example show the basic usage of create web server.
+
+```typescript
+import { createServer } from '@fastify/server'
+
+const server = createServer({}, function(request, response) {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+})
+server.listen({ port: 3000 })
+```
+
+## API
+
+## Difference from `node:http`, `node:https` and `node:http2`.
+
+### All-in-one `createServer`
+
+The `createServer` will determine using which `node` built-in server
+based on the options provided. You do not need to change the import
+module and just safely update the options accordingly.
+
+### Remove overload of `.listen`
+
+The `.listen` method only takes `options` and `callback` as arguments.
+It simplify the API and the process of normalize the arguments.
+
+### Solves `localhost` dual-stack problem
+
+`localhost` may resolve to either `IPv4` or `IPv6` based on operating
+system. Sometimes operating system will fallback to `IPv4` if you listen
+on `IPv6`. We instead listen on both to prevent this problem.
+
+|Host | IPv4 | IPv6 |
+|----------------------------------------------------------------------------------|----------------|------|
+|`::` | ✅* | ✅ |
+|`::` + [`ipv6Only`](https://nodejs.org/api/net.html#serverlistenoptions-callback) | 🚫 | ✅ |
+|`0.0.0.0` | ✅ | 🚫 |
+|`localhost` | ✅ | ✅ |
+|`127.0.0.1` | ✅ | 🚫 |
+|`::1` | 🚫 | ✅ |
+
+* Using `::` for the address will listen on all IPv6 addresses and,
+depending on OS, may also listen on [all IPv4
+addresses](https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback).
+
+### Dual `Promise API` and `Callback API`
+
+Unlike `Node.js` built-in, the `.listen` and `.close` will return `Promise`
+when you do not provide `callback` function. You can choose to use either
+API in your application.
+
+## License
+
+Licensed under [MIT](./LICENSE).
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..0af95fa
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": false
+ },
+ "files": {
+ "ignoreUnknown": false
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space"
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "preset": "recommended",
+ "correctness": {
+ "noInnerDeclarations": "off"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "includes": ["test/security.test.js"],
+ "linter": {
+ "rules": {
+ "suspicious": {
+ "noProto": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/lib/errors.js b/lib/errors.js
new file mode 100644
index 0000000..55c7f66
--- /dev/null
+++ b/lib/errors.js
@@ -0,0 +1,11 @@
+const { createError } = require("@fastify/error");
+
+/** @type {import('../types/errors.d.ts').ERR_SERVER_ALREADY_LISTEN} */
+const ERR_SERVER_ALREADY_LISTEN = createError(
+ "ERR_SERVER_ALREADY_LISTEN",
+ "Listen method has been called more than once without closing.",
+);
+
+module.exports = {
+ ERR_SERVER_ALREADY_LISTEN,
+};
diff --git a/lib/index.js b/lib/index.js
new file mode 100644
index 0000000..050982b
--- /dev/null
+++ b/lib/index.js
@@ -0,0 +1,199 @@
+const { selectServer, normalizeServerOptions } = require("./server");
+const { listenLookupHosts, normalizeListenOptions } = require("./listen");
+const {
+ normalizeCallback,
+ listeningOrigin,
+ stateClose,
+ syncInternalServers,
+} = require("./utils");
+const { kState, kRaw, kInternalServers } = require("./symbols");
+const { ERR_SERVER_ALREADY_LISTEN } = require("./errors");
+
+/** @typedef {import('../types/server').ServerOptions} ServerOptions */
+/** @typedef {import('../types/server').HTTPServerOptions} HTTPServerOptions */
+/** @typedef {import('../types/server').HTTPSServerOptions} HTTPSServerOptions */
+/** @typedef {import('../types/server').HTTP2ServerOptions} HTTP2ServerOptions */
+/** @typedef {import('../types/server').HTTP2SecureServerOptions} HTTP2SecureServerOptions */
+/** @typedef {import('../types/server').CustomServerOptions} CustomServerOptions */
+/** @typedef {import('../types/server').RequestListener} RequestListener */
+/** @typedef {import('../types/server').HTTPRequestListener} HTTPRequestListener */
+/** @typedef {import('../types/server').HTTPSRequestListener} HTTPSRequestListener */
+/** @typedef {import('../types/server').HTTP2RequestListener} HTTP2RequestListener */
+/** @typedef {import('../types/server').HTTP2SecureRequestListener} HTTP2SecureRequestListener */
+/** @typedef {import('../types/index').ProxyServer} ProxyServer */
+/** @typedef {import('../types/server').ProxyHTTPServer} ProxyHTTPServer */
+/** @typedef {import('../types/server').ProxyHTTPSServer} ProxyHTTPSServer */
+/** @typedef {import('../types/server').ProxyHTTP2Server} ProxyHTTP2Server */
+/** @typedef {import('../types/server').ProxyHTTP2SecureServer} ProxyHTTP2SecureServer */
+
+/**
+ * @overload
+ * @param {HTTPServerOptions} options
+ * @param {HTTPRequestListener} requestListener
+ * @returns {ProxyHTTPServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTPSServerOptions} options
+ * @param {HTTPSRequestListener} requestListener
+ * @returns {ProxyHTTPSServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTP2SecureServerOptions} options
+ * @param {HTTP2SecureRequestListener} requestListener
+ * @returns {ProxyHTTP2SecureServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTP2ServerOptions} options
+ * @param {HTTP2RequestListener} requestListener
+ * @returns {ProxyHTTP2Server}
+ */
+
+/**
+ * @overload
+ * @param {CustomServerOptions} options
+ * @param {RequestListener} requestListener
+ * @returns {ProxyServer}
+ */
+
+/**
+ *
+ * @param {ServerOptions} options
+ * @param {RequestListener} requestListener
+ * @returns {ProxyServer}
+ */
+function createServer(options, requestListener) {
+ const internalServers = [];
+ const serverOptions = normalizeServerOptions(options);
+ const { type, server } = selectServer(serverOptions, requestListener);
+ const state = {
+ listen: false,
+ listening: false,
+ closed: false,
+ closing: false,
+ error: false,
+ aborted: false,
+ };
+
+ // when there are internal servers
+ // proxy to sync between differece servers
+ const proxy = new Proxy(server, {
+ get(target, name, receiver) {
+ if (name === kRaw) return server;
+ if (name === kState) return state;
+ if (name === kInternalServers) return internalServers;
+ if (name === "listen") return listen;
+ if (name === "close") return close;
+ if (name === "addresses") return addresses;
+ if (name === "listeningOrigin")
+ return listeningOrigin(server, serverOptions);
+ return Reflect.get(target, name, receiver);
+ },
+ set(target, name, value, receiver) {
+ syncInternalServers(type, internalServers, name, value);
+ // always passthrough in last
+ return Reflect.set(target, name, value, receiver);
+ },
+ });
+
+ function listen(listenOptions = {}, callback) {
+ const cb = normalizeCallback(callback);
+ if (state.listen) {
+ cb(new ERR_SERVER_ALREADY_LISTEN());
+ return cb.promise;
+ }
+
+ const normalizedListenOptions = normalizeListenOptions(listenOptions);
+ // exit early when aborted
+ if (normalizedListenOptions.signal) {
+ if (normalizedListenOptions.signal.aborted) return;
+ state.aborted = normalizedListenOptions.signal.aborted;
+ normalizedListenOptions.signal.addEventListener(
+ "abort",
+ () => {
+ state.aborted = true;
+ close();
+ },
+ { once: true },
+ );
+ }
+
+ listenLookupHosts(
+ proxy,
+ requestListener,
+ serverOptions,
+ normalizedListenOptions,
+ (error) => {
+ if (error) {
+ state.listen = false;
+ cb(error);
+ } else {
+ state.listening = true;
+ state.closed = false;
+ // we do not override the server original event
+ // instead using custom prefix event to signal when all server is listening
+ server.emit("fastify.listening");
+ cb(null, server.address());
+ }
+ },
+ );
+
+ return cb.promise;
+ }
+
+ function close(callback) {
+ if (state.closed || state.closing) return;
+
+ const cb = normalizeCallback(callback);
+
+ function onClose() {
+ server.off("close", onClose);
+ let bound = internalServers.length;
+ if (bound === 0) {
+ stateClose(state, internalServers);
+ server.emit("fastify.close");
+ cb();
+ } else {
+ for (const internalServer of internalServers) {
+ function internalOnClose() {
+ internalServer.off("close", internalOnClose);
+ bound--;
+ if (bound === 0) {
+ stateClose(state, internalServers);
+ server.emit("fastify.close");
+ cb();
+ }
+ }
+ internalServer.on("close", internalOnClose);
+ }
+ }
+ }
+
+ state.closing = true;
+ // delay close callback to all server closed
+ server.on("close", onClose);
+ // server.close will signal the internalServer through close event
+ server.close();
+
+ return cb.promise;
+ }
+
+ function addresses() {
+ if (internalServers.length === 0) {
+ return [server.address()];
+ } else {
+ const addresses = internalServers.map((server) => server.address());
+ addresses.unshift(server.address());
+ return addresses;
+ }
+ }
+
+ return proxy;
+}
+
+module.exports.createServer = createServer;
diff --git a/lib/listen.js b/lib/listen.js
new file mode 100644
index 0000000..b19be7e
--- /dev/null
+++ b/lib/listen.js
@@ -0,0 +1,206 @@
+const { selectServer } = require("./server");
+const { kRaw, kInternalServers, kState } = require("./symbols");
+const { withResolvers } = require("./utils");
+
+/** @typedef {import('../types/listen').ListenOptions} ListenOptions */
+
+/**
+ * Normalize listen options to be compatible with Node.js server.listen() method.
+ * @param {ListenOptions} options - The options object to normalize.
+ * @returns {ListenOptions} - The normalized listen options.
+ */
+function normalizeListenOptions(options = {}) {
+ const listenOptions = {};
+ if (typeof options.handle === "object" && options.handle !== null) {
+ // handle take highest proirity
+ listenOptions.handle = options.handle;
+ } else if (typeof options.port === "number") {
+ // port take precedence over unix-socket
+ listenOptions.port = options.port;
+ listenOptions.host = options.host ?? "localhost";
+ } else if (typeof options.path === "string") {
+ // path is the lowest proirity
+ listenOptions.path = options.path;
+ } else {
+ // fallback to port/host when all option is not specified
+ listenOptions.port = options.port ?? 0;
+ listenOptions.host = options.host ?? "localhost";
+ }
+
+ // copy other options
+ // e.g. exclusive, readableAll, writableAll, ipv6Only, signal
+ if (typeof options.backlog === "number")
+ listenOptions.backlog = options.backlog;
+ if (typeof options.exclusive === "boolean")
+ listenOptions.exclusive = options.exclusive;
+ if (typeof options.ipv6Only === "boolean")
+ listenOptions.ipv6Only = options.ipv6Only;
+ if (typeof options.reusePort === "boolean")
+ listenOptions.reusePort = options.reusePort;
+ if (typeof options.readableAll === "boolean")
+ listenOptions.readableAll = options.readableAll;
+ if (typeof options.writableAll === "boolean")
+ listenOptions.writableAll = options.writableAll;
+ if (
+ options.signal !== null &&
+ typeof options.signal === "object" &&
+ typeof options.signal.addEventListener === "function"
+ ) {
+ listenOptions.signal = options.signal;
+ }
+
+ return listenOptions;
+}
+
+/** @typedef {import('../types/listen').LookupHostsCallback} LookupHostsCallback */
+/** @typedef {import("../types/index").ServerState} ServerState */
+
+/**
+ * Lookup the host addresses for the given listen options.
+ * @param {ListenOptions} listenOptions
+ * @param {ServerState} state
+ * @param {LookupHostsCallback} callback
+ * @returns {void}
+ */
+function lookupHosts(listenOptions, state, callback) {
+ // https://github.com/nodejs/node/issues/9390
+ // If listening to 'localhost', listen to both 127.0.0.1 or ::1 if they are available.
+ // If listening to 127.0.0.1, only listen to 127.0.0.1.
+ // If listening to ::1, only listen to ::1.
+ if (!listenOptions.host || listenOptions.host !== "localhost") {
+ return callback([]);
+ } else {
+ const dns = require("node:dns");
+ dns.lookup(listenOptions.host, { all: true }, (err, addresses) => {
+ if (err || state.aborted) {
+ // dns lookup failure should not affect the server listen
+ return callback([]);
+ }
+ callback(addresses);
+ });
+ }
+}
+
+/** @typedef {import('../types/listen').ListenLookupHostsCallback} ListenLookupHostsCallback */
+/** @typedef {import('../types/index').ProxyServer} ProxyServer */
+/** @typedef {import('../types/server').Server} Server */
+/** @typedef {import('../types/server').RequestListener} RequestListener */
+/** @typedef {import('../types/server').ServerOptions} ServerOptions */
+
+/**
+ * Lookup the host and listen
+ * @param {ProxyServer} proxy
+ * @param {RequestListener} requestHandler
+ * @param {ServerOptions} serverOptions
+ * @param {ListenOptions} listenOptions
+ * @param {ListenLookupHostsCallback} callback
+ */
+function listenLookupHosts(
+ proxy,
+ requestHandler,
+ serverOptions,
+ listenOptions,
+ callback,
+) {
+ server = proxy[kRaw];
+ state = proxy[kState];
+ const servers = proxy[kInternalServers];
+
+ if (!state.aborted) {
+ // signal.abort() will call server.close() implicitly
+ // and fire "close" event
+ // we need to remove the signal to prevent double fires
+ listenOptions.signal = undefined;
+ server.on("listening", onListening);
+ server.listen(listenOptions);
+ // state.listen = true when actually call server.listen
+ state.listen = true;
+ state.closed = false;
+ }
+
+ // use event listeners to handle callback
+ function cleanup() {
+ server.off("error", onError);
+ server.off("listening", onListening);
+ }
+ function onError(error) {
+ cleanup();
+ callback(error);
+ }
+ server.on("error", onError);
+ function onListening() {
+ cleanup();
+ const mainAddress = server.address();
+ const onUpgrade = server.emit.bind(server, "upgrade");
+ lookupHosts(listenOptions, state, (addresses) => {
+ const promises = [];
+
+ for (const address of addresses) {
+ // skip when the address is the same as the main address
+ if (address.address === mainAddress.address) continue;
+
+ const { promise, resolve, reject } = withResolvers();
+ promises.push(promise);
+
+ const newListenOptions = {
+ ...listenOptions,
+ host: address.address,
+ port: mainAddress.port,
+ };
+
+ const { server: newServer } = selectServer(
+ serverOptions,
+ requestHandler,
+ );
+
+ newServer.on("error", onError);
+ newServer.on("listening", onListening);
+
+ if (state.closing || state.closed || state.aborted) {
+ // promise will delay the child server listen to next event cycle
+ // we need to check if there are error or close on main server
+ // before listening
+ resolve();
+ } else {
+ newServer.listen(newListenOptions);
+ }
+
+ function closeServer() {
+ newServer.off("upgrade", onUpgrade);
+ newServer.off("close", closeServer);
+ server.off("error", closeServer);
+ newServer.close();
+ }
+
+ function cleanup() {
+ newServer.off("error", onError);
+ newServer.off("listening", onListening);
+ }
+
+ function onError(error) {
+ // secondary server listen failure should not affect the main server listen
+ cleanup();
+ reject(error);
+ }
+
+ function onListening() {
+ cleanup();
+ newServer.on("upgrade", onUpgrade);
+ server.on("close", closeServer);
+ server.on("error", closeServer);
+ servers.push(newServer);
+ resolve();
+ }
+ }
+
+ // use promise to parrallel wait internal server listen
+ // then unwrap promise when done
+ Promise.allSettled(promises).then(() => callback(null), callback);
+ });
+ }
+}
+
+module.exports = {
+ listenLookupHosts,
+ normalizeListenOptions,
+};
diff --git a/lib/server.js b/lib/server.js
new file mode 100644
index 0000000..a78b630
--- /dev/null
+++ b/lib/server.js
@@ -0,0 +1,263 @@
+/** @typedef {import('../types/server').ServerOptions} ServerOptions */
+
+/**
+ * normalize server options
+ * @param {ServerOptions} options
+ * @returns {ServerOptions}
+ */
+function normalizeServerOptions(options) {
+ const serverOptions = {};
+ if (typeof options.serverFactory === "function")
+ serverOptions.serverFactory = options.serverFactory;
+
+ if (options.http === true) serverOptions.http = {};
+ if (typeof options.http === "object" && options.http !== null)
+ serverOptions.http = options.http;
+
+ if (options.https === true) serverOptions.https = {};
+ if (typeof options.https === "object" && options.https !== null)
+ serverOptions.https = options.https;
+
+ if (options.http2 === true) serverOptions.http2 = true;
+
+ if (
+ typeof options.keepAliveTimeout === "number" &&
+ Number.isInteger(options.keepAliveTimeout)
+ )
+ serverOptions.keepAliveTimeout = options.keepAliveTimeout;
+ else serverOptions.keepAliveTimeout = 72000;
+
+ if (
+ typeof options.connectionTimeout === "number" &&
+ Number.isInteger(options.connectionTimeout)
+ )
+ serverOptions.connectionTimeout = options.connectionTimeout;
+ else serverOptions.connectionTimeout = 0;
+
+ if (
+ typeof options.maxRequestsPerSocket === "number" &&
+ Number.isInteger(options.maxRequestsPerSocket)
+ )
+ serverOptions.maxRequestsPerSocket = options.maxRequestsPerSocket;
+ else serverOptions.maxRequestsPerSocket = 0;
+
+ if (
+ typeof options.requestTimeout === "number" &&
+ Number.isInteger(options.requestTimeout)
+ )
+ serverOptions.requestTimeout = options.requestTimeout;
+ else serverOptions.requestTimeout = 0;
+
+ if (
+ typeof options.http2SessionTimeout === "number" &&
+ Number.isInteger(options.http2SessionTimeout)
+ )
+ serverOptions.http2SessionTimeout = options.http2SessionTimeout;
+ else serverOptions.http2SessionTimeout = 72000;
+
+ return serverOptions;
+}
+
+/** @typedef {import('../types/server').HTTPServerOptions} HTTPServerOptions */
+/** @typedef {import('../types/server').HTTPRequestListener} HTTPRequestListener */
+/** @typedef {import('../types/server').HTTPServer} HTTPServer */
+
+/**
+ *
+ * @param {HTTPServerOptions} options
+ * @param {HTTPRequestListener} requestListener
+ * @returns {HTTPServer}
+ */
+function createHTTPServer(options, requestListener) {
+ const http = require("node:http");
+
+ const server = http.createServer(options.http, requestListener);
+
+ server.keepAliveTimeout = options.keepAliveTimeout;
+ server.requestTimeout = options.requestTimeout;
+ server.setTimeout(options.connectionTimeout);
+ if (options.maxRequestsPerSocket > 0)
+ server.maxRequestsPerSocket = options.maxRequestsPerSocket;
+
+ return server;
+}
+
+/**
+ *
+ * @param {ServerOptions} options
+ * @returns {boolean}
+ */
+function isHTTPS(options) {
+ return typeof options.https === "object";
+}
+
+/** @typedef {import('../types/server').HTTPSServerOptions} HTTPSServerOptions */
+/** @typedef {import('../types/server').HTTPSRequestListener} HTTPSRequestListener */
+/** @typedef {import('../types/server').HTTPSServer} HTTPSServer */
+
+/**
+ *
+ * @param {HTTPSServerOptions} options
+ * @param {HTTPSRequestListener} requestListener
+ * @returns {HTTPSServer}
+ */
+function createHTTPSServer(options, requestListener) {
+ const https = require("node:https");
+
+ const server = https.createServer(options.https, requestListener);
+
+ server.keepAliveTimeout = options.keepAliveTimeout;
+ server.requestTimeout = options.requestTimeout;
+ server.setTimeout(options.connectionTimeout);
+ if (options.maxRequestsPerSocket > 0) {
+ server.maxRequestsPerSocket = options.maxRequestsPerSocket;
+ }
+
+ return server;
+}
+
+/**
+ *
+ * @param {ServerOptions} options
+ * @returns {boolean}
+ */
+function isHTTP2(options) {
+ return options.http2 === true;
+}
+
+/** @typedef {import('../types/server').HTTP2ServerOptions} HTTP2ServerOptions */
+/** @typedef {import('../types/server').HTTP2RequestListener} HTTP2RequestListener */
+/** @typedef {import('../types/server').HTTP2Server} HTTP2Server */
+/** @typedef {import('../types/server').HTTP2SecureServerOptions} HTTP2SecureServerOptions */
+/** @typedef {import('../types/server').HTTP2SecureRequestListener} HTTP2SecureRequestListener */
+/** @typedef {import('../types/server').HTTP2SecureServer} HTTP2SecureServer */
+
+/**
+ * @overload
+ * @param {HTTP2SecureServerOptions} options
+ * @param {HTTP2SecureRequestListener} requestListener
+ * @returns {HTTP2SecureServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTP2ServerOptions} options
+ * @param {HTTP2RequestListener} requestListener
+ * @returns {HTTP2Server}
+ */
+
+/**
+ *
+ * @param {HTTP2ServerOptions | HTTP2SecureServerOptions} options
+ * @param {HTTP2RequestListener | HTTP2SecureRequestListener} requestListener
+ * @returns {HTTP2Server | HTTP2SecureServer}
+ */
+function createHTTP2Server(options, requestListener) {
+ const http2 = require("node:http2");
+
+ let server = null;
+ if (isHTTPS(options)) {
+ server = http2.createSecureServer(options.https, requestListener);
+ } else {
+ server = http2.createServer(options.http ?? {}, requestListener);
+ }
+
+ // update options
+ server.on("session", (session) => {
+ session.setTimeout(options.http2SessionTimeout, () => {
+ session.close();
+ });
+ });
+
+ server.setTimeout(options.connectionTimeout);
+
+ return server;
+}
+
+/**
+ *
+ * @param {ServerOptions} options
+ * @returns {boolean}
+ */
+function isCustom(options) {
+ return options && typeof options.serverFactory === "function";
+}
+
+/** @typedef {import('../types/server').CustomServerOptions} CustomServerOptions */
+/** @typedef {import('../types/server').RequestListener} RequestListener */
+/** @typedef {import('../types/server').Server} Server */
+
+/**
+ *
+ * @param {CustomServerOptions} options
+ * @param {RequestListener} requestListener
+ * @returns {Server}
+ */
+function createCustomServer(options, requestListener) {
+ return options.serverFactory(requestListener, options);
+}
+
+/**
+ * @overload
+ * @param {HTTPServerOptions} options
+ * @param {HTTPRequestListener} requestListener
+ * @returns {HTTPServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTPSServerOptions} options
+ * @param {HTTPSRequestListener} requestListener
+ * @returns {HTTPSServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTP2SecureServerOptions} options
+ * @param {HTTP2SecureRequestListener} requestListener
+ * @returns {HTTP2SecureServer}
+ */
+
+/**
+ * @overload
+ * @param {HTTP2ServerOptions} options
+ * @param {HTTP2RequestListener} requestListener
+ * @returns {HTTP2Server}
+ */
+
+/**
+ * @overload
+ * @param {CustomServerOptions} options
+ * @param {RequestListener} requestListener
+ * @returns {Server}
+ */
+
+/**
+ *
+ * @param {ServerOptions} options
+ * @param {RequestListener} requestListener
+ * @returns {Server}
+ */
+function selectServer(options, requestListener) {
+ const result = { type: "", server: null };
+
+ if (isCustom(options)) {
+ result.type = "custom";
+ result.server = createCustomServer(options, requestListener);
+ } else if (isHTTP2(options)) {
+ result.type = "http2";
+ result.server = createHTTP2Server(options, requestListener);
+ } else if (isHTTPS(options)) {
+ result.type = "https";
+ result.server = createHTTPSServer(options, requestListener);
+ } else {
+ result.type = "http";
+ result.server = createHTTPServer(options, requestListener);
+ }
+
+ return result;
+}
+
+module.exports.isHTTPS = isHTTPS;
+module.exports.selectServer = selectServer;
+module.exports.normalizeServerOptions = normalizeServerOptions;
diff --git a/lib/symbols.js b/lib/symbols.js
new file mode 100644
index 0000000..d7122f5
--- /dev/null
+++ b/lib/symbols.js
@@ -0,0 +1,3 @@
+module.exports.kRaw = Symbol("fastify.server.raw");
+module.exports.kState = Symbol("fastify.server.state");
+module.exports.kInternalServers = Symbol("fastify.server.internal.servers");
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644
index 0000000..b54cfe2
--- /dev/null
+++ b/lib/utils.js
@@ -0,0 +1,91 @@
+const { isHTTPS } = require("./server");
+
+function normalizeCallback(callback) {
+ if (typeof callback !== "function") {
+ // promise chain should only happen on return
+ // internal API should always use callback
+ // to minimize the event cycle
+ const { promise, resolve, reject } = withResolvers();
+ function pCallback(error, data) {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(data);
+ }
+ }
+ pCallback.promise = promise;
+ return pCallback;
+ } else {
+ return callback;
+ }
+}
+
+// TODO(20.x): remove when node@20 is not supported
+function withResolvers() {
+ let res, rej;
+ const promise = new Promise((resolve, reject) => {
+ res = resolve;
+ rej = reject;
+ });
+ return { promise, resolve: res, reject: rej };
+}
+
+function listeningOrigin(server, options) {
+ const address = server.address();
+ if (typeof address === "string") return address;
+ const host =
+ address.family === "IPv6" ? `[${address.address}]` : address.address;
+ return `${isHTTPS(options) ? "https" : "http"}://${host}:${address.port}`;
+}
+
+function stateClose(state, internalServers) {
+ state.listen = false;
+ state.listening = false;
+ state.closed = true;
+ state.closing = false;
+ state.error = false;
+ state.aborted = false;
+ internalServers.length = 0;
+}
+
+const netProperties = new Set([
+ "maxConnections", // https://nodejs.org/docs/latest-v26.x/api/net.html#servermaxconnections
+ "dropMaxConnection", // https://nodejs.org/docs/latest-v26.x/api/net.html#serverdropmaxconnection
+]);
+const httpProperties = new Set([
+ ...netProperties,
+ "headersTimeout", // https://nodejs.org/docs/latest-v26.x/api/http.html#serverheaderstimeout
+ "maxHeadersCount", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxheaderscount
+ "requestTimeout", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxrequestspersocket
+ "maxRequestsPerSocket", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxrequestspersocket
+ "timeout", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxrequestspersocket
+ "keepAliveTimeout", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxrequestspersocket
+ "keepAliveTimeoutBuffer", // https://nodejs.org/docs/latest-v26.x/api/http.html#servermaxrequestspersocket
+]);
+const http2Properties = new Set([
+ ...netProperties,
+ "timeout", // https://nodejs.org/docs/latest-v26.x/api/http2.html#servertimeout
+]);
+const commonProperties = {
+ http: httpProperties,
+ https: httpProperties, // https.Server extends http.Server
+ http2: http2Properties,
+ custom: new Set(), // do not sync for custom server
+};
+
+function syncInternalServers(type, servers, name, value) {
+ const properites = commonProperties[type];
+ if (properites.has(name)) {
+ for (const server of servers) {
+ server[name] = value;
+ }
+ }
+}
+
+module.exports = {
+ normalizeCallback,
+ withResolvers,
+ listeningOrigin,
+ stateClose,
+ syncInternalServers,
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..83b605e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@fastify/server",
+ "version": "0.0.0",
+ "description": "A tool to create web server determine by options.",
+ "main": "./lib/index.js",
+ "type": "commonjs",
+ "types": "./types/index.d.ts",
+ "files": [
+ "lib/**/*",
+ "types/**/*"
+ ],
+ "scripts": {
+ "test": "npm run test:unit && npm run test:types",
+ "test:unit": "node --test",
+ "test:coverage": "c8 --100 npm run test:unit",
+ "test:types": "tstyche",
+ "lint": "npx @biomejs/biome check",
+ "lint:fix": "npx @biomejs/biome check --write"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/fastify/server.git"
+ },
+ "author": {
+ "name": "KaKa",
+ "email": "kaka@kakang.dev"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/fastify/server/issues"
+ },
+ "homepage": "https://github.com/fastify/server#readme",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "devDependencies": {
+ "@biomejs/biome": "2.5.6",
+ "@types/node": "^26.1.2",
+ "c8": "^12.0.0",
+ "node-forge": "^1.4.0",
+ "tstyche": "^7.2.2",
+ "typescript": "^7.0.2"
+ },
+ "dependencies": {
+ "@fastify/error": "^4.2.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "c8": {
+ "reporter": [
+ "html",
+ "text"
+ ]
+ }
+}
diff --git a/test/abort-signal.test.js b/test/abort-signal.test.js
new file mode 100644
index 0000000..d216bc3
--- /dev/null
+++ b/test/abort-signal.test.js
@@ -0,0 +1,96 @@
+"use strict";
+const { describe, test } = require("node:test");
+const { createServer } = require("../lib");
+const { once } = require("node:stream");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("abort signal", () => {
+ test("should close server when aborted after", async (t) => {
+ t.plan(1);
+
+ const controller = new AbortController();
+
+ const server = createServer({}, handler);
+ server.listen({ signal: controller.signal });
+
+ await once(server, "fastify.listening");
+ controller.abort();
+ await once(server, "fastify.close");
+ t.assert.strictEqual(server.listening, false);
+ });
+
+ test("should close server when aborted before", async (t) => {
+ t.plan(1);
+
+ const controller = new AbortController();
+ controller.abort();
+
+ const server = createServer({}, handler);
+ server.on("listening", () => {
+ t.assert.fail("should not reach");
+ });
+ server.on("close", () => {
+ t.assert.fail("should not reach");
+ });
+
+ server.listen({ signal: controller.signal });
+ t.assert.strictEqual(server.listening, false);
+ });
+
+ test("should not start server when aborted", async (t) => {
+ t.plan(2);
+
+ const controller = new AbortController();
+
+ const server = createServer({}, handler);
+ server.on("listening", () => {
+ t.assert.fail("should not reach");
+ });
+ // signal.abort() will call server.close explicitly
+ let closeCount = 0;
+ server.on("close", () => {
+ closeCount++;
+ t.assert.strictEqual(closeCount, 1);
+ });
+
+ server.listen({ signal: controller.signal });
+ controller.abort();
+
+ t.assert.strictEqual(server.listening, false);
+ await once(server, "close");
+ });
+
+ test("should not start server when already aborted", async (t) => {
+ t.plan(1);
+
+ const controller = new AbortController();
+
+ const server = createServer({}, handler);
+ server.on("listening", () => {
+ t.assert.fail("should not reach");
+ });
+ server.on("close", () => {
+ t.assert.fail("should not reach");
+ });
+
+ controller.abort();
+ server.listen({ signal: controller.signal });
+
+ t.assert.strictEqual(server.listening, false);
+ });
+
+ test("should ignore invalid signal", async (t) => {
+ t.plan(1);
+
+ const server = createServer({}, handler);
+ server.listen({ signal: { aborted: true } });
+ await once(server, "listening");
+ t.assert.strictEqual(server.listening, true);
+ server.close();
+ await once(server, "close");
+ });
+});
diff --git a/test/close/double-close.test.js b/test/close/double-close.test.js
new file mode 100644
index 0000000..76c2877
--- /dev/null
+++ b/test/close/double-close.test.js
@@ -0,0 +1,19 @@
+const { describe, test } = require("node:test");
+const { createServer } = require("../../lib");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("double close", () => {
+ test("should not emit close twice", async (t) => {
+ let count = 0;
+ const server = createServer({}, handler);
+ server.on("fastify.close", () => count++);
+ await server.listen();
+ await server.close();
+ await server.close();
+ t.assert.strictEqual(count, 1);
+ });
+});
diff --git a/test/close/http2-session-timeout.test.js b/test/close/http2-session-timeout.test.js
new file mode 100644
index 0000000..f523626
--- /dev/null
+++ b/test/close/http2-session-timeout.test.js
@@ -0,0 +1,89 @@
+"use strict";
+const { describe, test, before } = require("node:test");
+const { createServer } = require("../../lib/index");
+const { connect, constants } = require("node:http2");
+const { once } = require("node:stream");
+const { buildCertificate } = require("../utils");
+const { withResolvers } = require("../../lib/utils");
+
+describe("http2SessionTimeout", () => {
+ before(buildCertificate);
+
+ test("http2 close with async-await", async () => {
+ const server = createServer({
+ http2: true,
+ http2SessionTimeout: 100,
+ });
+ await server.listen();
+ const session = connect(server.listeningOrigin);
+ session.on("error", () => {});
+ await once(session, "connect");
+ await server.close();
+ });
+
+ test("http2s close with async-await", async () => {
+ const server = createServer({
+ http2: true,
+ https: {
+ key: global.context.privateKey,
+ cert: global.context.certificate,
+ },
+ http2SessionTimeout: 100,
+ });
+ await server.listen();
+ const session = connect(server.listeningOrigin, {
+ rejectUnauthorized: false,
+ });
+ session.on("error", () => {});
+ await once(session, "connect");
+ await server.close();
+ });
+
+ test("http2 server-side emit timeout ", async (t) => {
+ t.plan(1);
+
+ const server = createServer({
+ http2: true,
+ http2SessionTimeout: 100,
+ });
+ const { promise, resolve } = withResolvers();
+ server.on("stream", (stream) => {
+ stream.session.once("timeout", resolve);
+ stream.respond({
+ [constants.HTTP2_HEADER_STATUS]: 200,
+ [constants.HTTP2_HEADER_CONTENT_TYPE]:
+ "application/json; charset=utf-8",
+ });
+ stream.end("{}");
+ });
+ await server.listen();
+ const session = connect(server.listeningOrigin);
+ session.on("error", () => {});
+ await once(session, "connect");
+ const request = session.request({ ":method": "GET", ":path": "/" }).end();
+ const [headers] = await once(request, "response");
+ t.assert.strictEqual(headers[":status"], 200);
+ request.resume();
+ await promise;
+ await server.close();
+ });
+
+ test("http2 server-side emit timeout ", async (t) => {
+ t.plan(1);
+
+ const server = createServer({
+ http2: true,
+ http2SessionTimeout: 100,
+ });
+ await server.listen();
+ const waitSessionConnect = once(server, "session");
+ const session = connect(server.listeningOrigin);
+ session.on("error", () => {});
+ await once(session, "connect");
+ await waitSessionConnect;
+ const waitSessionClosed = once(session, "close");
+ await server.close();
+ await waitSessionClosed;
+ t.assert.strictEqual(session.closed, true);
+ });
+});
diff --git a/test/listen/dns.test.js b/test/listen/dns.test.js
new file mode 100644
index 0000000..4aea64c
--- /dev/null
+++ b/test/listen/dns.test.js
@@ -0,0 +1,136 @@
+"use strict";
+const { describe, test } = require("node:test");
+const dns = require("node:dns");
+const { createServer } = require("../../lib");
+const { withResolvers } = require("../../lib/utils");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("dns", () => {
+ test("throw error", async (t) => {
+ t.plan(4);
+
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ // node:http internally call dns.lookup to resolve localhost
+ // we need to alter the first call only
+ if (hostname === "localhost" && mock.callCount() === 1) {
+ callback(new Error("Kaboom!"));
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ mock.restore();
+ resolve();
+ });
+
+ server.listen();
+
+ await promise;
+ });
+
+ test("return empty record", async (t) => {
+ t.plan(4);
+
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ // node:http internally call dns.lookup to resolve localhost
+ // we need to alter the first call only
+ if (hostname === "localhost" && mock.callCount() === 1) {
+ callback(null, []);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ mock.restore();
+ resolve();
+ });
+
+ server.listen();
+
+ await promise;
+ });
+
+ test("return > 2 records", async (t) => {
+ t.plan(4);
+
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ // node:http internally call dns.lookup to resolve localhost
+ // we need to alter the first call only
+ if (hostname === "localhost" && mock.callCount() === 1) {
+ callback(null, [
+ { address: "::1", family: 6 },
+ { address: "127.0.0.1", family: 4 },
+ { address: "0.0.0.0", family: 4 },
+ ]);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 3);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ mock.restore();
+ resolve();
+ });
+
+ server.listen();
+
+ await promise;
+ });
+});
diff --git a/test/listen/error.test.js b/test/listen/error.test.js
new file mode 100644
index 0000000..289b2b5
--- /dev/null
+++ b/test/listen/error.test.js
@@ -0,0 +1,180 @@
+"use strict";
+const { describe, test } = require("node:test");
+const { once } = require("node:stream");
+const http = require("node:http");
+const { createServer } = require("../../lib");
+const { withResolvers } = require("../../lib/utils");
+const { nextTick } = require("node:process");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("error", () => {
+ describe("ERR_SERVER_ALREADY_LISTEN", () => {
+ test(".listen()", async (t) => {
+ t.plan(5);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({}, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.listen({}, (error) => {
+ t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN");
+ server.close();
+ });
+ });
+
+ await promise;
+ });
+
+ test(".listen({ host: '127.0.0.1' })", async (t) => {
+ t.plan(5);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ host: "127.0.0.1" }, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.listen({ host: "127.0.0.1" }, (error) => {
+ t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN");
+ server.close();
+ });
+ });
+
+ await promise;
+ });
+
+ test("await .listen()", async (t) => {
+ t.plan(3);
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ await server.listen();
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+
+ try {
+ await server.listen();
+ } catch (error) {
+ t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN");
+ } finally {
+ await server.close();
+ }
+ });
+
+ test("await .listen({ host: '127.0.0.1' })", async (t) => {
+ t.plan(3);
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ await server.listen({ host: "127.0.0.1" });
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+
+ try {
+ await server.listen({ host: "127.0.0.1" });
+ } catch (error) {
+ t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN");
+ } finally {
+ await server.close();
+ }
+ });
+ });
+
+ describe("EADDRINUSE", () => {
+ test("listen conflict address and port", async (t) => {
+ t.plan(3);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ const conflict = createServer({}, handler);
+
+ server.listen({}, (error, address) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+
+ conflict.listen({ port: address.port }, (error) => {
+ t.assert.strictEqual(error.code, "EADDRINUSE");
+ server.close(resolve);
+ });
+ });
+
+ await promise;
+ });
+
+ test("allow listen after conflict", async (t) => {
+ t.plan(3);
+ const { promise, resolve } = withResolvers();
+ const net = require("node:net");
+ const conflict = net.createServer();
+ conflict.listen({ port: 0, host: "127.0.0.1" });
+ await once(conflict, "listening");
+
+ const server = createServer({}, handler);
+
+ const listenOptions = {
+ port: conflict.address().port,
+ host: "127.0.0.1",
+ };
+ server.listen(listenOptions, (error) => {
+ t.assert.strictEqual(error.code, "EADDRINUSE");
+
+ conflict.close();
+
+ server.listen(listenOptions, (error) => {
+ t.assert.ifError(error);
+ t.assert.strictEqual(server.listening, true);
+
+ server.close(resolve);
+ });
+ });
+
+ await promise;
+ });
+ });
+
+ test("second server listen error", async (t) => {
+ t.plan(1);
+
+ const httpCreateServer = http.createServer;
+ let count = 0;
+ const { mock } = t.mock.method(http, "createServer", (...args) => {
+ const server = httpCreateServer(...args);
+ if (count === 1) {
+ // delay to simulate the error throw in new server
+ nextTick(() => {
+ server.emit("error", new Error("kaboom!"));
+ server.close();
+ });
+ }
+ count++;
+ return server;
+ });
+ const server = createServer({}, handler);
+ await server.listen();
+ t.assert.strictEqual(server.addresses().length, 1);
+ await server.close();
+ mock.restore();
+ });
+});
diff --git a/test/listen/host.test.js b/test/listen/host.test.js
new file mode 100644
index 0000000..edc5b25
--- /dev/null
+++ b/test/listen/host.test.js
@@ -0,0 +1,229 @@
+"use strict";
+const { describe, test } = require("node:test");
+const { createServer } = require("../../lib");
+const { withResolvers } = require("../../lib/utils");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("host/port", () => {
+ test(".listen()", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen();
+
+ await promise;
+ });
+
+ test(".listen(undefined)", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen(undefined);
+
+ await promise;
+ });
+
+ test(".listen(null)", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen(undefined);
+
+ await promise;
+ });
+
+ test(".listen({ host: '::1' })", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ host: "::1" });
+
+ await promise;
+ });
+
+ test(".listen({ host: '127.0.0.1' })", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ host: "127.0.0.1" });
+
+ await promise;
+ });
+
+ test(".listen({ host: null })", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ host: null });
+
+ await promise;
+ });
+
+ test(".listen({ host: undefined })", async (t) => {
+ t.plan(4);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ host: undefined });
+
+ await promise;
+ });
+
+ test(".listen({ ... })", async (t) => {
+ t.plan(5);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen(
+ {
+ port: 0,
+ host: "localhost",
+ backlog: 511,
+ exclusive: false,
+ readableAll: false,
+ writableAll: false,
+ ipv6Only: false,
+ },
+ (error) => {
+ t.assert.ifError(error);
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ },
+ );
+
+ await promise;
+ });
+
+ test(".listen({}, callback)", async (t) => {
+ t.plan(5);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({}, (error) => {
+ t.assert.ifError(error);
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close();
+ });
+
+ await promise;
+ });
+});
diff --git a/test/listen/listen-after-close.test.js b/test/listen/listen-after-close.test.js
new file mode 100644
index 0000000..99fe675
--- /dev/null
+++ b/test/listen/listen-after-close.test.js
@@ -0,0 +1,65 @@
+"use strict";
+const { describe, test } = require("node:test");
+const { createServer } = require("../../lib");
+const { withResolvers } = require("../../lib/utils");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("listen again after close", () => {
+ test(".listen()", async (t) => {
+ t.plan(6);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.listen({}, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+
+ server.close(() => {
+ t.assert.strictEqual(server.listening, false);
+
+ server.listen({}, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 2);
+ server.close(resolve);
+ });
+ });
+ });
+
+ await promise;
+ });
+
+ test(".listen({ host: '127.0.0.1' })", async (t) => {
+ t.plan(6);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.listen({ host: "127.0.0.1" }, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+
+ server.close(() => {
+ t.assert.strictEqual(server.listening, false);
+
+ server.listen({ host: "127.0.0.1" }, (error) => {
+ t.assert.ifError(error);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ server.close(resolve);
+ });
+ });
+ });
+
+ await promise;
+ });
+});
diff --git a/test/listen/normalize.test.js b/test/listen/normalize.test.js
new file mode 100644
index 0000000..7d35c96
--- /dev/null
+++ b/test/listen/normalize.test.js
@@ -0,0 +1,155 @@
+"use strict";
+const { describe, test } = require("node:test");
+const { normalizeListenOptions } = require("../../lib/listen");
+const { Socket } = require("node:net");
+
+describe("listen options normalize", () => {
+ const handle = Socket();
+ const abort = new AbortController();
+
+ const cases = [
+ {
+ name: "{ handle }",
+ actual: { handle },
+ expect: { handle },
+ },
+ {
+ name: "{ handle, path }",
+ actual: { handle, path: "" },
+ expect: { handle },
+ },
+ {
+ name: "{ handle, host }",
+ actual: { handle, host: "127.0.0.1" },
+ expect: { handle },
+ },
+ {
+ name: "{ handle, host, port }",
+ actual: { handle, host: "127.0.0.1", port: 1 },
+ expect: { handle },
+ },
+ {
+ name: "{ path }",
+ actual: { path: "" },
+ expect: { path: "" },
+ },
+ {
+ name: "{ path, host }",
+ actual: { path: "", host: "127.0.0.1" },
+ expect: { path: "" },
+ },
+ {
+ name: "{ path, port }",
+ actual: { path: "", port: 1 },
+ expect: { host: "localhost", port: 1 },
+ },
+ {
+ name: "{ path, host, port }",
+ actual: { path: "", host: "127.0.0.1", port: 1 },
+ expect: { host: "127.0.0.1", port: 1 },
+ },
+ {
+ name: "{ port }",
+ actual: { port: 1 },
+ expect: { host: "localhost", port: 1 },
+ },
+ {
+ name: "{ host }",
+ actual: { host: "127.0.0.1" },
+ expect: { host: "127.0.0.1", port: 0 },
+ },
+ {
+ name: "{ host, port }",
+ actual: { host: "127.0.0.1", port: 1 },
+ expect: { host: "127.0.0.1", port: 1 },
+ },
+ {
+ name: "{ backlog: 1 }",
+ actual: { backlog: 1 },
+ expect: { host: "localhost", port: 0, backlog: 1 },
+ },
+ {
+ name: "{ backlog: true }",
+ actual: { backlog: true },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ exclusive: 1 }",
+ actual: { exclusive: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ exclusive: true }",
+ actual: { exclusive: true },
+ expect: { host: "localhost", port: 0, exclusive: true },
+ },
+ {
+ name: "{ ipv6Only: 1 }",
+ actual: { ipv6Only: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ ipv6Only: true }",
+ actual: { ipv6Only: true },
+ expect: { host: "localhost", port: 0, ipv6Only: true },
+ },
+ {
+ name: "{ reusePort: 1 }",
+ actual: { reusePort: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ reusePort: true }",
+ actual: { reusePort: true },
+ expect: { host: "localhost", port: 0, reusePort: true },
+ },
+ {
+ name: "{ readableAll: 1 }",
+ actual: { readableAll: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ readableAll: true }",
+ actual: { readableAll: true },
+ expect: { host: "localhost", port: 0, readableAll: true },
+ },
+ {
+ name: "{ writableAll: 1 }",
+ actual: { writableAll: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ writableAll: true }",
+ actual: { writableAll: true },
+ expect: { host: "localhost", port: 0, writableAll: true },
+ },
+ {
+ name: "{ signal: 1 }",
+ actual: { signal: 1 },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ signal: {} }",
+ actual: { signal: {} },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ signal: null }",
+ actual: { signal: null },
+ expect: { host: "localhost", port: 0 },
+ },
+ {
+ name: "{ signal }",
+ actual: { signal: abort.signal },
+ expect: { host: "localhost", port: 0, signal: abort.signal },
+ },
+ ];
+
+ for (const { name, actual, expect } of cases) {
+ test(name, (t) => {
+ t.plan(1);
+ const result = normalizeListenOptions(actual);
+ t.assert.deepStrictEqual(result, expect);
+ });
+ }
+});
diff --git a/test/listen/socket.test.js b/test/listen/socket.test.js
new file mode 100644
index 0000000..707920c
--- /dev/null
+++ b/test/listen/socket.test.js
@@ -0,0 +1,68 @@
+"use strict";
+const { describe, test } = require("node:test");
+const os = require("node:os");
+const path = require("node:path");
+const fs = require("node:fs");
+const { createServer } = require("../../lib");
+const { withResolvers } = require("../../lib/utils");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("socket", () => {
+ let sockPath = "";
+ if (os.platform() !== "win32") {
+ sockPath = path.join(
+ os.tmpdir(),
+ `${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server.sock`,
+ );
+ try {
+ fs.unlinkSync(sockFile);
+ } catch {}
+ } else {
+ sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`;
+ }
+
+ test(".listen({ path })", { skip: !sockPath }, async (t) => {
+ t.plan(5);
+ const { promise, resolve } = withResolvers();
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ server.once("fastify.listening", () => {
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ t.assert.deepStrictEqual(addresses, [sockPath]);
+ server.close();
+ });
+ server.once("fastify.close", () => {
+ t.assert.strictEqual(server.listening, false);
+ resolve();
+ });
+
+ server.listen({ path: sockPath });
+
+ await promise;
+ });
+
+ // Refs: https://github.com/fastify/fastify/pull/6937
+ test(".listen({ path, host })", { skip: !sockPath }, async (t) => {
+ t.plan(4);
+
+ const server = createServer({}, handler);
+ t.assert.strictEqual(server.listening, false);
+
+ await server.listen({ path: sockPath, host: "localhost" });
+
+ t.assert.strictEqual(server.listening, true);
+ const addresses = server.addresses();
+ t.assert.strictEqual(addresses.length, 1);
+ t.assert.deepStrictEqual(addresses, [sockPath]);
+
+ await server.close();
+ });
+});
diff --git a/test/listening-origin.test.js b/test/listening-origin.test.js
new file mode 100644
index 0000000..262282e
--- /dev/null
+++ b/test/listening-origin.test.js
@@ -0,0 +1,140 @@
+"use strict";
+const { describe, test } = require("node:test");
+const dns = require("node:dns");
+const os = require("node:os");
+const path = require("node:path");
+const fs = require("node:fs");
+const { createServer } = require("../lib");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("listeningOrigin", () => {
+ test("ipv4", async (t) => {
+ t.plan(1);
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ if (hostname === "localhost") {
+ callback(null, [{ address: "127.0.0.1", family: 4 }]);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const server = createServer({}, handler);
+ await server.listen();
+ const address = server.address();
+ t.assert.strictEqual(
+ server.listeningOrigin,
+ `http://127.0.0.1:${address.port}`,
+ );
+ await server.close();
+ mock.restore();
+ });
+
+ test("ipv6", async (t) => {
+ t.plan(1);
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ if (hostname === "localhost") {
+ callback(null, [{ address: "::1", family: 6 }]);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const server = createServer({}, handler);
+ await server.listen();
+ const address = server.address();
+ t.assert.strictEqual(
+ server.listeningOrigin,
+ `http://[::1]:${address.port}`,
+ );
+ await server.close();
+ mock.restore();
+ });
+
+ let sockPath = "";
+ if (os.platform() !== "win32") {
+ sockPath = path.join(
+ os.tmpdir(),
+ `${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server.sock`,
+ );
+ try {
+ fs.unlinkSync(sockFile);
+ } catch {}
+ } else {
+ sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`;
+ }
+
+ test("path", { skip: !sockPath }, async (t) => {
+ t.plan(1);
+
+ const server = createServer({}, handler);
+ await server.listen({ path: sockPath });
+ t.assert.strictEqual(server.listeningOrigin, sockPath);
+ await server.close();
+ });
+
+ test("https + ipv4", async (t) => {
+ t.plan(1);
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ if (hostname === "localhost") {
+ callback(null, [{ address: "127.0.0.1", family: 4 }]);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const server = createServer({ https: {} }, handler);
+ await server.listen();
+ const address = server.address();
+ t.assert.strictEqual(
+ server.listeningOrigin,
+ `https://127.0.0.1:${address.port}`,
+ );
+ await server.close();
+ mock.restore();
+ });
+
+ test("ipv6", async (t) => {
+ t.plan(1);
+ const lookup = dns.lookup;
+ const { mock } = t.mock.method(
+ dns,
+ "lookup",
+ (hostname, options, callback) => {
+ if (hostname === "localhost") {
+ callback(null, [{ address: "::1", family: 6 }]);
+ } else {
+ lookup(hostname, options, callback);
+ }
+ },
+ );
+
+ const server = createServer({ https: {} }, handler);
+ await server.listen();
+ const address = server.address();
+ t.assert.strictEqual(
+ server.listeningOrigin,
+ `https://[::1]:${address.port}`,
+ );
+ await server.close();
+ mock.restore();
+ });
+});
diff --git a/test/options/connection-timeout.test.js b/test/options/connection-timeout.test.js
new file mode 100644
index 0000000..99f9ae3
--- /dev/null
+++ b/test/options/connection-timeout.test.js
@@ -0,0 +1,83 @@
+"use strict";
+const http = require("node:http");
+const { describe, test } = require("node:test");
+const { once } = require("node:stream");
+const { createServer } = require("../../lib");
+const { kInternalServers } = require("../../lib/symbols");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("connectionTimeout", () => {
+ test("[]", (t) => {
+ t.plan(1);
+ const server = createServer({ connectionTimeout: [] });
+ t.assert.strictEqual(server.timeout, 0);
+ });
+
+ test("1.3", (t) => {
+ t.plan(1);
+ const server = createServer({ connectionTimeout: 1.3 });
+ t.assert.strictEqual(server.timeout, 0);
+ });
+
+ test("http", (t) => {
+ t.plan(1);
+ const server = createServer({ connectionTimeout: 1 });
+ t.assert.strictEqual(server.timeout, 1);
+ });
+
+ test("https", (t) => {
+ t.plan(1);
+ const server = createServer({ connectionTimeout: 2, https: {} });
+ t.assert.strictEqual(server.timeout, 2);
+ });
+
+ test("http2", (t) => {
+ t.plan(1);
+ const server = createServer({ connectionTimeout: 3, http2: true });
+ t.assert.strictEqual(server.timeout, 3);
+ });
+
+ test("http2 + https", (t) => {
+ t.plan(1);
+ const server = createServer({
+ connectionTimeout: 3,
+ http2: true,
+ https: {},
+ });
+ t.assert.strictEqual(server.timeout, 3);
+ });
+
+ test("serverFactory", (t) => {
+ t.plan(1);
+
+ function serverFactory(requestHandler) {
+ const server = http.createServer((request, response) => {
+ requestHandler(request, response);
+ });
+ server.setTimeout(5);
+ return server;
+ }
+
+ const server = createServer({ connectionTimeout: 4, serverFactory });
+ t.assert.strictEqual(server.timeout, 5);
+ });
+
+ test("update all servers", async (t) => {
+ t.plan(3);
+ const server = createServer({ connectionTimeout: 1 }, handler);
+ t.assert.strictEqual(server.timeout, 1);
+ server.listen();
+ await once(server, "fastify.listening");
+ server.setTimeout(5);
+ t.assert.strictEqual(server.timeout, 5);
+ for (const internal of server[kInternalServers]) {
+ t.assert.strictEqual(internal.timeout, 5);
+ }
+ server.close();
+ await once(server, "fastify.close");
+ });
+});
diff --git a/test/options/keep-alive-timeout.test.js b/test/options/keep-alive-timeout.test.js
new file mode 100644
index 0000000..616b75c
--- /dev/null
+++ b/test/options/keep-alive-timeout.test.js
@@ -0,0 +1,83 @@
+"use strict";
+const http = require("node:http");
+const { describe, test } = require("node:test");
+const { once } = require("node:stream");
+const { createServer } = require("../../lib");
+const { kInternalServers } = require("../../lib/symbols");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("keepAliveTimeout", () => {
+ test("[]", (t) => {
+ t.plan(1);
+ const server = createServer({ keepAliveTimeout: [] });
+ t.assert.strictEqual(server.keepAliveTimeout, 72000);
+ });
+
+ test("1.3", (t) => {
+ t.plan(1);
+ const server = createServer({ keepAliveTimeout: 1.3 });
+ t.assert.strictEqual(server.keepAliveTimeout, 72000);
+ });
+
+ test("http", (t) => {
+ t.plan(1);
+ const server = createServer({ keepAliveTimeout: 1 });
+ t.assert.strictEqual(server.keepAliveTimeout, 1);
+ });
+
+ test("https", (t) => {
+ t.plan(1);
+ const server = createServer({ keepAliveTimeout: 2, https: {} });
+ t.assert.strictEqual(server.keepAliveTimeout, 2);
+ });
+
+ test("http2", (t) => {
+ t.plan(1);
+ const server = createServer({ keepAliveTimeout: 3, http2: true });
+ t.assert.notStrictEqual(server.keepAliveTimeout, 3);
+ });
+
+ test("http2 + https", (t) => {
+ t.plan(1);
+ const server = createServer({
+ keepAliveTimeout: 3,
+ http2: true,
+ https: {},
+ });
+ t.assert.notStrictEqual(server.keepAliveTimeout, 3);
+ });
+
+ test("serverFactory", (t) => {
+ t.plan(1);
+
+ function serverFactory(requestHandler) {
+ const server = http.createServer((request, response) => {
+ requestHandler(request, response);
+ });
+ server.keepAliveTimeout = 5;
+ return server;
+ }
+
+ const server = createServer({ keepAliveTimeout: 4, serverFactory });
+ t.assert.strictEqual(server.keepAliveTimeout, 5);
+ });
+
+ test("update all servers", async (t) => {
+ t.plan(3);
+ const server = createServer({ keepAliveTimeout: 1 }, handler);
+ t.assert.strictEqual(server.keepAliveTimeout, 1);
+ server.listen();
+ await once(server, "fastify.listening");
+ server.keepAliveTimeout = 5;
+ t.assert.strictEqual(server.keepAliveTimeout, 5);
+ for (const internal of server[kInternalServers]) {
+ t.assert.strictEqual(internal.keepAliveTimeout, 5);
+ }
+ server.close();
+ await once(server, "fastify.close");
+ });
+});
diff --git a/test/options/max-requests-per-socket.test.js b/test/options/max-requests-per-socket.test.js
new file mode 100644
index 0000000..587788d
--- /dev/null
+++ b/test/options/max-requests-per-socket.test.js
@@ -0,0 +1,83 @@
+"use strict";
+const http = require("node:http");
+const { describe, test } = require("node:test");
+const { once } = require("node:stream");
+const { createServer } = require("../../lib");
+const { kInternalServers } = require("../../lib/symbols");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("maxRequestsPerSocket", () => {
+ test("[]", (t) => {
+ t.plan(1);
+ const server = createServer({ maxRequestsPerSocket: [] });
+ t.assert.strictEqual(server.maxRequestsPerSocket, 0);
+ });
+
+ test("1.3", (t) => {
+ t.plan(1);
+ const server = createServer({ maxRequestsPerSocket: 1.3 });
+ t.assert.strictEqual(server.maxRequestsPerSocket, 0);
+ });
+
+ test("http", (t) => {
+ t.plan(1);
+ const server = createServer({ maxRequestsPerSocket: 1, http: {} });
+ t.assert.strictEqual(server.maxRequestsPerSocket, 1);
+ });
+
+ test("https", (t) => {
+ t.plan(1);
+ const server = createServer({ maxRequestsPerSocket: 2, https: {} });
+ t.assert.strictEqual(server.maxRequestsPerSocket, 2);
+ });
+
+ test("http2", (t) => {
+ t.plan(1);
+ const server = createServer({ maxRequestsPerSocket: 3, http2: true });
+ t.assert.notStrictEqual(server.maxRequestsPerSocket, 3);
+ });
+
+ test("http2 + https", (t) => {
+ t.plan(1);
+ const server = createServer({
+ maxRequestsPerSocket: 3,
+ http2: true,
+ https: {},
+ });
+ t.assert.notStrictEqual(server.maxRequestsPerSocket, 3);
+ });
+
+ test("serverFactory", (t) => {
+ t.plan(1);
+
+ function serverFactory(requestHandler) {
+ const server = http.createServer((request, response) => {
+ requestHandler(request, response);
+ });
+ server.maxRequestsPerSocket = 5;
+ return server;
+ }
+
+ const server = createServer({ maxRequestsPerSocket: 4, serverFactory });
+ t.assert.strictEqual(server.maxRequestsPerSocket, 5);
+ });
+
+ test("update all servers", async (t) => {
+ t.plan(3);
+ const server = createServer({ maxRequestsPerSocket: 1 }, handler);
+ t.assert.strictEqual(server.maxRequestsPerSocket, 1);
+ server.listen();
+ await once(server, "fastify.listening");
+ server.maxRequestsPerSocket = 5;
+ t.assert.strictEqual(server.maxRequestsPerSocket, 5);
+ for (const internal of server[kInternalServers]) {
+ t.assert.strictEqual(internal.maxRequestsPerSocket, 5);
+ }
+ server.close();
+ await once(server, "fastify.close");
+ });
+});
diff --git a/test/options/request-timeout.test.js b/test/options/request-timeout.test.js
new file mode 100644
index 0000000..b632f68
--- /dev/null
+++ b/test/options/request-timeout.test.js
@@ -0,0 +1,79 @@
+"use strict";
+const http = require("node:http");
+const { describe, test } = require("node:test");
+const { once } = require("node:stream");
+const { createServer } = require("../../lib");
+const { kInternalServers } = require("../../lib/symbols");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("requestTimeout", () => {
+ test("[]", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: [] });
+ t.assert.strictEqual(server.requestTimeout, 0);
+ });
+
+ test("1.3", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: 1.3 });
+ t.assert.strictEqual(server.requestTimeout, 0);
+ });
+
+ test("http", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: 1, http: true });
+ t.assert.strictEqual(server.requestTimeout, 1);
+ });
+
+ test("https", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: 2, https: true });
+ t.assert.strictEqual(server.requestTimeout, 2);
+ });
+
+ test("http2", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: 3, http2: true });
+ t.assert.notStrictEqual(server.requestTimeout, 3);
+ });
+
+ test("http2 + https", (t) => {
+ t.plan(1);
+ const server = createServer({ requestTimeout: 3, http2: true, https: {} });
+ t.assert.notStrictEqual(server.requestTimeout, 3);
+ });
+
+ test("serverFactory", (t) => {
+ t.plan(1);
+
+ function serverFactory(requestHandler) {
+ const server = http.createServer((request, response) => {
+ requestHandler(request, response);
+ });
+ server.requestTimeout = 5;
+ return server;
+ }
+
+ const server = createServer({ requestTimeout: 4, serverFactory });
+ t.assert.strictEqual(server.requestTimeout, 5);
+ });
+
+ test("update all servers", async (t) => {
+ t.plan(3);
+ const server = createServer({ requestTimeout: 1 }, handler);
+ t.assert.strictEqual(server.requestTimeout, 1);
+ server.listen();
+ await once(server, "fastify.listening");
+ server.requestTimeout = 5;
+ t.assert.strictEqual(server.requestTimeout, 5);
+ for (const internal of server[kInternalServers]) {
+ t.assert.strictEqual(internal.requestTimeout, 5);
+ }
+ server.close();
+ await once(server, "fastify.close");
+ });
+});
diff --git a/test/security.test.js b/test/security.test.js
new file mode 100644
index 0000000..75bb722
--- /dev/null
+++ b/test/security.test.js
@@ -0,0 +1,25 @@
+"use strict";
+const { describe, test } = require("node:test");
+const http = require("node:http");
+const { createServer } = require("../lib");
+
+const handler = (_request, response) => {
+ response.writeHead(200, { "Content-Type": "application/json" });
+ response.end(JSON.stringify({ data: "Hello World!" }));
+};
+
+describe("security", () => {
+ test("__proto__ should be Node built-in server", (t) => {
+ const server = createServer({}, handler);
+ const nodeServer = http.createServer({}, handler);
+
+ t.assert.deepStrictEqual(server.__proto__, nodeServer.__proto__);
+ });
+
+ test("prototype should be Node built-in server", (t) => {
+ const server = createServer({}, handler);
+ const nodeServer = http.createServer({}, handler);
+
+ t.assert.deepStrictEqual(server.prototype, nodeServer.prototype);
+ });
+});
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..ac8b4d2
--- /dev/null
+++ b/test/utils.js
@@ -0,0 +1,35 @@
+"use strict";
+const { hostname } = require("node:os");
+const { pki } = require("node-forge");
+
+function buildCertificate() {
+ if (!global.context?.certificate || !global.context?.privateKey) {
+ const now = new Date();
+ const { publicKey, privateKey } = pki.rsa.generateKeyPair(2048);
+ const certificate = pki.createCertificate();
+ certificate.publicKey = publicKey;
+ certificate.serialNumber = "01";
+ certificate.validity.notBefore = now;
+ certificate.validity.notAfter = new Date(Number(now) + 86400000);
+ const attrs = [
+ { shortName: "CN", value: hostname() },
+ { shortName: "C", value: "US" },
+ { shortName: "ST", value: "California" },
+ { shortName: "L", value: "San Francisco" },
+ { shortName: "O", value: "Fastify" },
+ { shortName: "OU", value: "Development Team" },
+ ];
+ certificate.setSubject(attrs);
+ certificate.setIssuer(attrs);
+
+ certificate.sign(privateKey);
+
+ global.context = {
+ privateKey: pki.privateKeyToPem(privateKey),
+ publicKey: pki.publicKeyToPem(publicKey),
+ certificate: pki.certificateToPem(certificate),
+ };
+ }
+}
+
+module.exports.buildCertificate = buildCertificate;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..58484d3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "include": ["./types"],
+ "compilerOptions": {
+ "types": ["node"],
+ "allowJs": true,
+ "noEmit": true
+ }
+}
diff --git a/types/errors.d.ts b/types/errors.d.ts
new file mode 100644
index 0000000..9a833f3
--- /dev/null
+++ b/types/errors.d.ts
@@ -0,0 +1,6 @@
+import type { FastifyErrorConstructor } from "@fastify/error";
+
+export const ERR_SERVER_ALREADY_LISTEN: FastifyErrorConstructor<
+ { code: "ERR_SERVER_ALREADY_LISTEN" },
+ []
+>;
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..1870b01
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,66 @@
+import type { AddressInfo } from "node:net";
+import type { kInternalServers, kRaw, kState } from "../lib/symbols";
+import type {
+ CustomServerOptions,
+ HTTP2RequestListener,
+ HTTP2SecureRequestListener,
+ HTTP2SecureServer,
+ HTTP2SecureServerOptions,
+ HTTP2Server,
+ HTTP2ServerOptions,
+ HTTPRequestListener,
+ HTTPServer,
+ HTTPServerOptions,
+ HTTPSRequestListener,
+ HTTPSServer,
+ HTTPSServerOptions,
+ RequestListener,
+} from "./server";
+
+export interface ServerState {
+ listen: boolean;
+ listening: boolean;
+ closed: boolean;
+ closing: boolean;
+ error: boolean;
+ aborted: boolean;
+}
+
+interface ProxyProperties {
+ [kRaw]: T;
+ [kState]: ServerState;
+ [kInternalServers]: Array;
+ addresses: () => Array;
+}
+
+export type ProxyHTTPServer = HTTPServer & ProxyProperties;
+export type ProxyHTTPSServer = HTTPSServer & ProxyProperties;
+export type ProxyHTTP2Server = HTTP2Server & ProxyProperties;
+export type ProxyHTTP2SecureServer = HTTP2SecureServer &
+ ProxyProperties;
+export type ProxyServer =
+ | ProxyHTTPServer
+ | ProxyHTTPSServer
+ | ProxyHTTP2Server
+ | ProxyHTTP2SecureServer;
+
+export function createServer(
+ options: T,
+ requestListener: RequestListener,
+): ProxyHTTP2SecureServer;
+export function createServer(
+ options: T,
+ requestListener: HTTP2SecureRequestListener,
+): ProxyHTTP2SecureServer;
+export function createServer(
+ options: T,
+ requestListener: HTTP2RequestListener,
+): ProxyHTTP2Server;
+export function createServer(
+ options: T,
+ requestListener: HTTPSRequestListener,
+): ProxyHTTPSServer;
+export function createServer(
+ options: T,
+ requestListener: HTTPRequestListener,
+): ProxyHTTPServer;
diff --git a/types/index.tst.ts b/types/index.tst.ts
new file mode 100644
index 0000000..b26b4aa
--- /dev/null
+++ b/types/index.tst.ts
@@ -0,0 +1,20 @@
+import type { AddressInfo } from "node:net";
+import { expect } from "tstyche";
+import {
+ createServer,
+ type ProxyHTTP2SecureServer,
+ type ProxyHTTP2Server,
+ type ProxyHTTPServer,
+ type ProxyHTTPSServer,
+} from ".";
+
+expect(createServer({}, () => {})).type.toBe();
+expect(createServer({ https: true }, () => {})).type.toBe();
+expect(createServer({ https: {} }, () => {})).type.toBe();
+expect(createServer({ http2: true }, () => {})).type.toBe();
+expect(
+ createServer({ http2: true, https: {} }, () => {}),
+).type.toBe();
+
+const proxy = createServer({}, () => {});
+expect(proxy.addresses()).type.toBe>();
diff --git a/types/listen.d.ts b/types/listen.d.ts
new file mode 100644
index 0000000..25b05b5
--- /dev/null
+++ b/types/listen.d.ts
@@ -0,0 +1,29 @@
+import type { AddressInfo, ListenOptions as NetListenOptions } from "node:net";
+import { ProxyServer, ServerState } from ".";
+import { RequestListener, ServerOptions } from "./server";
+
+export interface ListenOptions extends NetListenOptions {}
+
+export type NormalizeListenOptionsFn = (
+ options?: ListenOptions,
+) => ListenOptions;
+
+export type LookupHostsCallback = (
+ addresses: Array,
+) => void;
+
+export type LookupHostsFn = (
+ listenOptions: ListenOptions,
+ state: ServerState,
+ callback: LookupHostsCallback,
+) => void;
+
+export type ListenLookupHostsCallback = (error?: null | Error) => void;
+
+export type ListenLookupHostsFn = (
+ proxy: ProxyServer,
+ requestHandler: RequestListener,
+ serverOptions: ServerOptions,
+ listenOptions: ListenOptions,
+ callback: ListenLookupHostsCallback,
+) => void;
diff --git a/types/server.d.ts b/types/server.d.ts
new file mode 100644
index 0000000..93323cd
--- /dev/null
+++ b/types/server.d.ts
@@ -0,0 +1,80 @@
+import type {
+ Server as NodeHTTPServer,
+ ServerOptions as NodeHTTPServerOptions,
+ RequestListener as NodeRequestListener,
+} from "node:http";
+import type {
+ Http2SecureServer as NodeHTTP2SecureServer,
+ SecureServerOptions as NodeHTTP2SecureServerOptions,
+ Http2Server as NodeHTTP2Server,
+ ServerOptions as NodeHTTP2ServerOptions,
+} from "node:http2";
+import type {
+ Server as NodeHTTPSServer,
+ ServerOptions as NodeHTTPSServerOptions,
+} from "node:https";
+
+interface CommonServerOptions {
+ /** @default 72000 */
+ keepAliveTimeout?: number;
+ /** @default 0 */
+ connectionTimeout?: number;
+ /** @default 0 */
+ maxRequestsPerSocket?: number;
+ /** @default 0 */
+ requestTimeout?: number;
+}
+
+export interface HTTPServerOptions extends CommonServerOptions {
+ http?: true | NodeHTTPServerOptions;
+ https?: false;
+ http2?: false;
+}
+export type HTTPRequestListener = NodeRequestListener;
+export type HTTPServer = NodeHTTPServer;
+
+export interface HTTPSServerOptions extends CommonServerOptions {
+ http?: false;
+ https: true | NodeHTTPSServerOptions;
+ http2?: false;
+}
+export type HTTPSRequestListener = NodeRequestListener;
+export type HTTPSServer = NodeHTTPSServer;
+
+export interface HTTP2ServerOptions extends CommonServerOptions {
+ http?: NodeHTTP2ServerOptions;
+ https?: false;
+ http2: true;
+ /** @default 72000 */
+ http2SessionTimeout?: number;
+}
+export type HTTP2RequestListener = NodeRequestListener;
+export type HTTP2Server = NodeHTTP2Server;
+
+export interface HTTP2SecureServerOptions extends CommonServerOptions {
+ http?: false;
+ https: NodeHTTP2SecureServerOptions;
+ http2: true;
+}
+export type HTTP2SecureRequestListener = NodeRequestListener;
+export type HTTP2SecureServer = NodeHTTP2SecureServer;
+
+export interface CustomServerOptions extends CommonServerOptions {
+ serverFactory: (
+ requestListener: RequestListener,
+ options: ServerOptions,
+ ) => Server;
+}
+
+export type ServerOptions =
+ | HTTPServerOptions
+ | HTTPSServerOptions
+ | HTTP2ServerOptions
+ | HTTP2SecureServerOptions
+ | CustomServerOptions;
+export type RequestListener =
+ | HTTPRequestListener
+ | HTTPSRequestListener
+ | HTTP2RequestListener
+ | HTTP2SecureRequestListener;
+export type Server = HTTPServer | HTTPSServer | HTTP2Server | HTTP2SecureServer;
diff --git a/types/utils.d.ts b/types/utils.d.ts
new file mode 100644
index 0000000..36d9a1b
--- /dev/null
+++ b/types/utils.d.ts
@@ -0,0 +1,9 @@
+type Callback = (error?: null | Error, data?: T) => void;
+
+export function normalizeCallback(callback?: Callback): Callback;
+
+export function withResolvers(): {
+ promise: Promise;
+ resolve: (data?: T) => void;
+ reject: (error?: null | Error) => void;
+};