From 7887823e761da95a091ce3cd77487c5bbe9a4c5e Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 4 Aug 2026 23:33:55 +0800 Subject: [PATCH 01/13] feat: initial implementation --- .gitattributes | 2 + .github/dependabot.yml | 45 +++++ .github/workflows/ci.yml | 33 +++ .github/workflows/lock-threads.yml | 19 ++ .gitignore | 143 +++++++++++++ .npmrc | 3 + README.md | 65 ++++++ biome.json | 37 ++++ lib/errors.js | 11 + lib/index.js | 216 ++++++++++++++++++++ lib/listen.js | 199 +++++++++++++++++++ lib/server.js | 254 ++++++++++++++++++++++++ lib/symbols.js | 3 + lib/utils.js | 34 ++++ package.json | 56 ++++++ test/abort-signal.test.js | 95 +++++++++ test/listen/dns.test.js | 135 +++++++++++++ test/listen/error.test.js | 154 ++++++++++++++ test/listen/host.test.js | 228 +++++++++++++++++++++ test/listen/listen-after-close.test.js | 64 ++++++ test/listen/socket.test.js | 50 +++++ test/options/keep-alive-timeout.test.js | 72 +++++++ tsconfig.json | 8 + types/errors.d.ts | 6 + types/index.d.ts | 66 ++++++ types/index.tst.ts | 20 ++ types/listen.d.ts | 29 +++ types/server.d.ts | 78 ++++++++ types/utils.d.ts | 5 + 29 files changed, 2130 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/lock-threads.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 biome.json create mode 100644 lib/errors.js create mode 100644 lib/index.js create mode 100644 lib/listen.js create mode 100644 lib/server.js create mode 100644 lib/symbols.js create mode 100644 lib/utils.js create mode 100644 package.json create mode 100644 test/abort-signal.test.js create mode 100644 test/listen/dns.test.js create mode 100644 test/listen/error.test.js create mode 100644 test/listen/host.test.js create mode 100644 test/listen/listen-after-close.test.js create mode 100644 test/listen/socket.test.js create mode 100644 test/options/keep-alive-timeout.test.js create mode 100644 tsconfig.json create mode 100644 types/errors.d.ts create mode 100644 types/index.d.ts create mode 100644 types/index.tst.ts create mode 100644 types/listen.d.ts create mode 100644 types/server.d.ts create mode 100644 types/utils.d.ts 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..e7c1a63 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +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" + # TypeScript related dependencies + dev-dependencies-typescript: + patterns: + - "@types/*" + - "tstyche" + - "typescript" + # Development dependencies with breaking changes + dev-dependencies: + dependency-type: "development" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..64ad0bf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +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@2073dc8e1f9e172bf42daa3843c9dbd31af1e8cb # v6.0.0 + with: + 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..15dec1b --- /dev/null +++ b/biome.json @@ -0,0 +1,37 @@ +{ + "$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" + } + } + } +} 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..9b78386 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,216 @@ +const { selectServer } = require("./server"); +const { listenLookupHosts, normalizeListenOptions } = require("./listen"); +const { normalizeCallback, withResolvers } = require("./utils"); +const { kState, kRaw, kInternalServers } = require("./symbols"); +const { ERR_SERVER_ALREADY_LISTEN } = require("./errors"); + +const commonProperty = new Set([ + "keepAliveTimeout", + "headersTimeout", + "maxHeadersCount", + "requestTimeout", + "maxRequestsPerSocket", + "timeout", + "keepAliveTimeoutBuffer", +]); + +/** @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 server = selectServer(options, requestListener); + const state = { + listen: false, + listening: false, + closed: false, + closing: false, + error: false, + aborted: false, + }; + + function stateClose() { + state.listen = false; + state.listening = false; + state.closed = true; + state.closing = false; + state.error = false; + state.aborted = false; + internalServers.length = 0; + } + + // 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; + return Reflect.get(target, name, receiver); + }, + set(target, name, value, receiver) { + // set common property + if (commonProperty.has(name)) { + for (const server of internalServers) { + server[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, + options, + 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(); + server.emit("fastify.close"); + cb() + } else { + for (const internalServer of internalServers) { + function internalOnClose() { + internalServer.off("close", internalOnClose); + bound--; + if (bound === 0) { + stateClose(); + 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..e3d7ff5 --- /dev/null +++ b/lib/listen.js @@ -0,0 +1,199 @@ +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.path === "string") { + // unix-socket take precedence over host/port + listenOptions.path = options.path; + } else { + 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 ( + 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 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(); + if (serverOptions.forceCloseConnections === true) + newServer.closeAllConnections(); + } + + 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..0e683ca --- /dev/null +++ b/lib/server.js @@ -0,0 +1,254 @@ +/** @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; + + 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 serverOptions = options.https === true ? {} : options.https; + + const server = https.createServer(serverOptions, 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 serverOptions = normalizeServerOptions(options); + + if (isCustom(serverOptions)) { + return createCustomServer(serverOptions, requestListener); + } + + if (isHTTP2(serverOptions)) { + return createHTTP2Server(serverOptions, requestListener); + } + + if (isHTTPS(serverOptions)) { + return createHTTPSServer(serverOptions, requestListener); + } + + return createHTTPServer(serverOptions, requestListener); +} + +module.exports.selectServer = selectServer; 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..a70c648 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,34 @@ +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 callback(error, data) { + if (error) { + reject(error) + } else { + resolve(data) + } + } + callback.promise = promise + return callback + } 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 }; +} + +module.exports = { + normalizeCallback, + withResolvers, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a9f2ed8 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "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 node --test", + "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", + "tstyche": "^7.2.2", + "typescript": "^7.0.2" + }, + "dependencies": { + "@fastify/error": "^4.2.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/test/abort-signal.test.js b/test/abort-signal.test.js new file mode 100644 index 0000000..b1db664 --- /dev/null +++ b/test/abort-signal.test.js @@ -0,0 +1,95 @@ +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/listen/dns.test.js b/test/listen/dns.test.js new file mode 100644 index 0000000..a078aa0 --- /dev/null +++ b/test/listen/dns.test.js @@ -0,0 +1,135 @@ +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..674d210 --- /dev/null +++ b/test/listen/error.test.js @@ -0,0 +1,154 @@ +const { describe, test } = require("node:test"); +const { once } = require("node:stream"); +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("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; + }); + }); +}); diff --git a/test/listen/host.test.js b/test/listen/host.test.js new file mode 100644 index 0000000..b636d06 --- /dev/null +++ b/test/listen/host.test.js @@ -0,0 +1,228 @@ +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..c9e0d78 --- /dev/null +++ b/test/listen/listen-after-close.test.js @@ -0,0 +1,64 @@ +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/socket.test.js b/test/listen/socket.test.js new file mode 100644 index 0000000..d584456 --- /dev/null +++ b/test/listen/socket.test.js @@ -0,0 +1,50 @@ +const { describe, test } = require("node:test"); +const { createServer } = require("../../lib"); +const { withResolvers } = require("../../lib/utils"); +const os = require("node:os"); +const path = require("node:path"); +const fs = require("node:fs"); + +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; + }); +}); diff --git a/test/options/keep-alive-timeout.test.js b/test/options/keep-alive-timeout.test.js new file mode 100644 index 0000000..cffcd11 --- /dev/null +++ b/test/options/keep-alive-timeout.test.js @@ -0,0 +1,72 @@ +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("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/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..689864a --- /dev/null +++ b/types/server.d.ts @@ -0,0 +1,78 @@ +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; +} +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..98cd142 --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,5 @@ +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 } \ No newline at end of file From 0822e471fd2e0e1462d6b99c530b615c6fa50bda Mon Sep 17 00:00:00 2001 From: KaKa Date: Tue, 4 Aug 2026 23:50:40 +0800 Subject: [PATCH 02/13] fixup --- lib/index.js | 22 +++++++++++----------- lib/utils.js | 14 +++++++------- test/listen/error.test.js | 9 ++++----- types/utils.d.ts | 10 +++++++--- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/index.js b/lib/index.js index 9b78386..dc978ba 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ const { selectServer } = require("./server"); const { listenLookupHosts, normalizeListenOptions } = require("./listen"); -const { normalizeCallback, withResolvers } = require("./utils"); +const { normalizeCallback } = require("./utils"); const { kState, kRaw, kInternalServers } = require("./symbols"); const { ERR_SERVER_ALREADY_LISTEN } = require("./errors"); @@ -119,10 +119,10 @@ function createServer(options, requestListener) { }); function listen(listenOptions = {}, callback) { - const cb = normalizeCallback(callback) + const cb = normalizeCallback(callback); if (state.listen) { - cb(new ERR_SERVER_ALREADY_LISTEN()) - return cb.promise + cb(new ERR_SERVER_ALREADY_LISTEN()); + return cb.promise; } const normalizedListenOptions = normalizeListenOptions(listenOptions); @@ -148,25 +148,25 @@ function createServer(options, requestListener) { (error) => { if (error) { state.listen = false; - cb(error) + 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()) + cb(null, server.address()); } }, ); - return cb.promise + return cb.promise; } function close(callback) { if (state.closed || state.closing) return; - const cb = normalizeCallback(callback) + const cb = normalizeCallback(callback); function onClose() { server.off("close", onClose); @@ -174,7 +174,7 @@ function createServer(options, requestListener) { if (bound === 0) { stateClose(); server.emit("fastify.close"); - cb() + cb(); } else { for (const internalServer of internalServers) { function internalOnClose() { @@ -183,7 +183,7 @@ function createServer(options, requestListener) { if (bound === 0) { stateClose(); server.emit("fastify.close"); - cb() + cb(); } } internalServer.on("close", internalOnClose); @@ -197,7 +197,7 @@ function createServer(options, requestListener) { // server.close will signal the internalServer through close event server.close(); - return cb.promise + return cb.promise; } function addresses() { diff --git a/lib/utils.js b/lib/utils.js index a70c648..41b848a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -3,18 +3,18 @@ function normalizeCallback(callback) { // promise chain should only happen on return // internal API should always use callback // to minimize the event cycle - const { promise, resolve, reject } = withResolvers() - function callback(error, data) { + const { promise, resolve, reject } = withResolvers(); + function pCallback(error, data) { if (error) { - reject(error) + reject(error); } else { - resolve(data) + resolve(data); } } - callback.promise = promise - return callback + pCallback.promise = promise; + return pCallback; } else { - return callback + return callback; } } diff --git a/test/listen/error.test.js b/test/listen/error.test.js index 674d210..1ed02a9 100644 --- a/test/listen/error.test.js +++ b/test/listen/error.test.js @@ -60,19 +60,18 @@ describe("error", () => { await promise; }); - test("await .listen()", async (t) => { t.plan(3); const server = createServer({}, handler); t.assert.strictEqual(server.listening, false); - await server.listen() + await server.listen(); const addresses = server.addresses(); t.assert.strictEqual(addresses.length, 2); try { - await server.listen() + await server.listen(); } catch (error) { t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN"); } finally { @@ -85,12 +84,12 @@ describe("error", () => { const server = createServer({}, handler); t.assert.strictEqual(server.listening, false); - await server.listen({ host: '127.0.0.1' }) + 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' }) + await server.listen({ host: "127.0.0.1" }); } catch (error) { t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN"); } finally { diff --git a/types/utils.d.ts b/types/utils.d.ts index 98cd142..36d9a1b 100644 --- a/types/utils.d.ts +++ b/types/utils.d.ts @@ -1,5 +1,9 @@ -type Callback = (error?: null | Error, data?: T) => void +type Callback = (error?: null | Error, data?: T) => void; -export function normalizeCallback(callback?: Callback): Callback +export function normalizeCallback(callback?: Callback): Callback; -export function withResolvers(): { promise: Promise, resolve: (data?: T) => void, reject: (error?: null | Error) => void } \ No newline at end of file +export function withResolvers(): { + promise: Promise; + resolve: (data?: T) => void; + reject: (error?: null | Error) => void; +}; From bcdfb2253cb0e6a0bee7e456a2d50fcf9578ddab Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 5 Aug 2026 00:31:54 +0800 Subject: [PATCH 03/13] feat: listeningOrigin --- biome.json | 14 +++++- lib/index.js | 17 ++----- lib/utils.js | 20 ++++++++ test/listen/socket.test.js | 4 +- test/listening-origin.test.js | 87 +++++++++++++++++++++++++++++++++++ test/security.test.js | 24 ++++++++++ 6 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 test/listening-origin.test.js create mode 100644 test/security.test.js diff --git a/biome.json b/biome.json index 15dec1b..0af95fa 100644 --- a/biome.json +++ b/biome.json @@ -33,5 +33,17 @@ "organizeImports": "on" } } - } + }, + "overrides": [ + { + "includes": ["test/security.test.js"], + "linter": { + "rules": { + "suspicious": { + "noProto": "off" + } + } + } + } + ] } diff --git a/lib/index.js b/lib/index.js index dc978ba..53b1ad7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ const { selectServer } = require("./server"); const { listenLookupHosts, normalizeListenOptions } = require("./listen"); -const { normalizeCallback } = require("./utils"); +const { normalizeCallback, listeningOrigin, stateClose } = require("./utils"); const { kState, kRaw, kInternalServers } = require("./symbols"); const { ERR_SERVER_ALREADY_LISTEN } = require("./errors"); @@ -84,16 +84,6 @@ function createServer(options, requestListener) { aborted: false, }; - function stateClose() { - state.listen = false; - state.listening = false; - state.closed = true; - state.closing = false; - state.error = false; - state.aborted = false; - internalServers.length = 0; - } - // when there are internal servers // proxy to sync between differece servers const proxy = new Proxy(server, { @@ -104,6 +94,7 @@ function createServer(options, requestListener) { if (name === "listen") return listen; if (name === "close") return close; if (name === "addresses") return addresses; + if (name === "listeningOrigin") return listeningOrigin(server, options); return Reflect.get(target, name, receiver); }, set(target, name, value, receiver) { @@ -172,7 +163,7 @@ function createServer(options, requestListener) { server.off("close", onClose); let bound = internalServers.length; if (bound === 0) { - stateClose(); + stateClose(state, internalServers); server.emit("fastify.close"); cb(); } else { @@ -181,7 +172,7 @@ function createServer(options, requestListener) { internalServer.off("close", internalOnClose); bound--; if (bound === 0) { - stateClose(); + stateClose(state, internalServers); server.emit("fastify.close"); cb(); } diff --git a/lib/utils.js b/lib/utils.js index 41b848a..12c8c95 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -28,7 +28,27 @@ function withResolvers() { 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 `${options.https ? "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; +} + module.exports = { normalizeCallback, withResolvers, + listeningOrigin, + stateClose, }; diff --git a/test/listen/socket.test.js b/test/listen/socket.test.js index d584456..abd90b7 100644 --- a/test/listen/socket.test.js +++ b/test/listen/socket.test.js @@ -1,9 +1,9 @@ const { describe, test } = require("node:test"); -const { createServer } = require("../../lib"); -const { withResolvers } = require("../../lib/utils"); 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" }); diff --git a/test/listening-origin.test.js b/test/listening-origin.test.js new file mode 100644 index 0000000..33794cf --- /dev/null +++ b/test/listening-origin.test.js @@ -0,0 +1,87 @@ +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(); + }); +}); diff --git a/test/security.test.js b/test/security.test.js new file mode 100644 index 0000000..f9cca68 --- /dev/null +++ b/test/security.test.js @@ -0,0 +1,24 @@ +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); + }); +}); From b93cd75ad55472128ba969a81fe73f3be0fe5092 Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 13 Aug 2026 04:41:57 +0800 Subject: [PATCH 04/13] fixup --- lib/index.js | 28 +--- lib/listen.js | 16 +- lib/server.js | 53 ++++--- lib/utils.js | 39 ++++- package.json | 11 +- test/abort-signal.test.js | 1 + test/close/double-close.test.js | 19 +++ test/close/http2-session-timeout.test.js | 91 +++++++++++ test/listen/dns.test.js | 1 + test/listen/error.test.js | 27 ++++ test/listen/host.test.js | 1 + test/listen/listen-after-close.test.js | 1 + test/listen/normalize.test.js | 156 +++++++++++++++++++ test/listen/socket.test.js | 22 ++- test/listening-origin.test.js | 55 ++++++- test/options/connection-timeout.test.js | 79 ++++++++++ test/options/keep-alive-timeout.test.js | 7 + test/options/max-requests-per-socket.test.js | 79 ++++++++++ test/options/request-timeout.test.js | 79 ++++++++++ test/security.test.js | 1 + test/utils.js | 36 +++++ types/server.d.ts | 2 + 22 files changed, 754 insertions(+), 50 deletions(-) create mode 100644 test/close/double-close.test.js create mode 100644 test/close/http2-session-timeout.test.js create mode 100644 test/listen/normalize.test.js create mode 100644 test/options/connection-timeout.test.js create mode 100644 test/options/max-requests-per-socket.test.js create mode 100644 test/options/request-timeout.test.js create mode 100644 test/utils.js diff --git a/lib/index.js b/lib/index.js index 53b1ad7..75c4b73 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,19 +1,9 @@ -const { selectServer } = require("./server"); +const { selectServer, normalizeServerOptions } = require("./server"); const { listenLookupHosts, normalizeListenOptions } = require("./listen"); -const { normalizeCallback, listeningOrigin, stateClose } = require("./utils"); +const { normalizeCallback, listeningOrigin, stateClose, syncInternalServers } = require("./utils"); const { kState, kRaw, kInternalServers } = require("./symbols"); const { ERR_SERVER_ALREADY_LISTEN } = require("./errors"); -const commonProperty = new Set([ - "keepAliveTimeout", - "headersTimeout", - "maxHeadersCount", - "requestTimeout", - "maxRequestsPerSocket", - "timeout", - "keepAliveTimeoutBuffer", -]); - /** @typedef {import('../types/server').ServerOptions} ServerOptions */ /** @typedef {import('../types/server').HTTPServerOptions} HTTPServerOptions */ /** @typedef {import('../types/server').HTTPSServerOptions} HTTPSServerOptions */ @@ -74,7 +64,8 @@ const commonProperty = new Set([ */ function createServer(options, requestListener) { const internalServers = []; - const server = selectServer(options, requestListener); + const serverOptions = normalizeServerOptions(options); + const { type, server } = selectServer(serverOptions, requestListener); const state = { listen: false, listening: false, @@ -94,16 +85,11 @@ function createServer(options, requestListener) { if (name === "listen") return listen; if (name === "close") return close; if (name === "addresses") return addresses; - if (name === "listeningOrigin") return listeningOrigin(server, options); + if (name === "listeningOrigin") return listeningOrigin(server, serverOptions); return Reflect.get(target, name, receiver); }, set(target, name, value, receiver) { - // set common property - if (commonProperty.has(name)) { - for (const server of internalServers) { - server[name] = value; - } - } + syncInternalServers(type, internalServers, name, value) // always passthrough in last return Reflect.set(target, name, value, receiver); }, @@ -134,7 +120,7 @@ function createServer(options, requestListener) { listenLookupHosts( proxy, requestListener, - options, + serverOptions, normalizedListenOptions, (error) => { if (error) { diff --git a/lib/listen.js b/lib/listen.js index e3d7ff5..f274e0c 100644 --- a/lib/listen.js +++ b/lib/listen.js @@ -14,12 +14,17 @@ function normalizeListenOptions(options = {}) { if (typeof options.handle === "object" && options.handle !== null) { // handle take highest proirity listenOptions.handle = options.handle; - } else if (typeof options.path === "string") { - // unix-socket take precedence over host/port + } 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"; + listenOptions.host = options.host ?? 'localhost' } // copy other options @@ -37,6 +42,7 @@ function normalizeListenOptions(options = {}) { if (typeof options.writableAll === "boolean") listenOptions.writableAll = options.writableAll; if ( + options.signal !== null && typeof options.signal === "object" && typeof options.signal.addEventListener === "function" ) { @@ -142,7 +148,7 @@ function listenLookupHosts( port: mainAddress.port, }; - const newServer = selectServer(serverOptions, requestHandler); + const { server: newServer } = selectServer(serverOptions, requestHandler); newServer.on("error", onError); newServer.on("listening", onListening); @@ -161,8 +167,6 @@ function listenLookupHosts( newServer.off("close", closeServer); server.off("error", closeServer); newServer.close(); - if (serverOptions.forceCloseConnections === true) - newServer.closeAllConnections(); } function cleanup() { diff --git a/lib/server.js b/lib/server.js index 0e683ca..7f386c0 100644 --- a/lib/server.js +++ b/lib/server.js @@ -25,28 +25,41 @@ function normalizeServerOptions(options) { Number.isInteger(options.keepAliveTimeout) ) serverOptions.keepAliveTimeout = options.keepAliveTimeout; - else serverOptions.keepAliveTimeout = 72000; + else + serverOptions.keepAliveTimeout = 72000; if ( typeof options.connectionTimeout === "number" && Number.isInteger(options.connectionTimeout) ) serverOptions.connectionTimeout = options.connectionTimeout; - else serverOptions.connectionTimeout = 0; + else + serverOptions.connectionTimeout = 0; if ( typeof options.maxRequestsPerSocket === "number" && Number.isInteger(options.maxRequestsPerSocket) ) serverOptions.maxRequestsPerSocket = options.maxRequestsPerSocket; - else serverOptions.maxRequestsPerSocket = 0; + else + serverOptions.maxRequestsPerSocket = 0; if ( typeof options.requestTimeout === "number" && Number.isInteger(options.requestTimeout) ) serverOptions.requestTimeout = options.requestTimeout; - else serverOptions.requestTimeout = 0; + else + serverOptions.requestTimeout = 0; + + if ( + typeof options.http2SessionTimeout === "number" && + Number.isInteger(options.http2SessionTimeout) + ) + serverOptions.http2SessionTimeout = options.http2SessionTimeout; + else + serverOptions.http2SessionTimeout = 72000; + return serverOptions; } @@ -152,7 +165,7 @@ function createHTTP2Server(options, requestListener) { let server = null; if (isHTTPS(options)) { - server = http2.createSecureServer(options.https ?? {}, requestListener); + server = http2.createSecureServer(options.https, requestListener); } else { server = http2.createServer(options.http ?? {}, requestListener); } @@ -234,21 +247,25 @@ function createCustomServer(options, requestListener) { * @returns {Server} */ function selectServer(options, requestListener) { - const serverOptions = normalizeServerOptions(options); - - if (isCustom(serverOptions)) { - return createCustomServer(serverOptions, requestListener); - } - - if (isHTTP2(serverOptions)) { - return createHTTP2Server(serverOptions, requestListener); - } - - if (isHTTPS(serverOptions)) { - return createHTTPSServer(serverOptions, 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 createHTTPServer(serverOptions, requestListener); + return result; } +module.exports.isHTTPS = isHTTPS; module.exports.selectServer = selectServer; +module.exports.normalizeServerOptions = normalizeServerOptions; diff --git a/lib/utils.js b/lib/utils.js index 12c8c95..89ce38d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,5 @@ +const { isHTTPS } = require("./server"); + function normalizeCallback(callback) { if (typeof callback !== "function") { // promise chain should only happen on return @@ -33,7 +35,7 @@ function listeningOrigin(server, options) { if (typeof address === "string") return address; const host = address.family === "IPv6" ? `[${address.address}]` : address.address; - return `${options.https ? "https" : "http"}://${host}:${address.port}`; + return `${isHTTPS(options) ? "https" : "http"}://${host}:${address.port}`; } function stateClose(state, internalServers) { @@ -46,9 +48,44 @@ function stateClose(state, internalServers) { 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 index a9f2ed8..b4a99a3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ ], "scripts": { "test": "npm run test:unit && npm run test:types", - "test:unit": "node --test", - "test:coverage": "c8 node --test", + "test:unit": "node --test --test-timeout=500", + "test:coverage": "c8 npm run test:unit", "test:types": "tstyche", "lint": "npx @biomejs/biome check", "lint:fix": "npx @biomejs/biome check --write" @@ -44,6 +44,7 @@ "@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" }, @@ -52,5 +53,11 @@ }, "publishConfig": { "access": "public" + }, + "c8": { + "reporter": [ + "html", + "text" + ] } } diff --git a/test/abort-signal.test.js b/test/abort-signal.test.js index b1db664..449f170 100644 --- a/test/abort-signal.test.js +++ b/test/abort-signal.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const { createServer } = require("../lib"); const { once } = require("node:stream"); diff --git a/test/close/double-close.test.js b/test/close/double-close.test.js new file mode 100644 index 0000000..18b382e --- /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) + }) +}) \ No newline at end of file diff --git a/test/close/http2-session-timeout.test.js b/test/close/http2-session-timeout.test.js new file mode 100644 index 0000000..76fc1bf --- /dev/null +++ b/test/close/http2-session-timeout.test.js @@ -0,0 +1,91 @@ +'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"); + +const handler = (_request, response) => { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ data: "Hello World!" })); +}; + +describe('http2SessionTimeout', () => { + before(buildCertificate) + + test('http2 close with async-await', async (t) => { + 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 (t) => { + 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, headers, flags) => { + 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) + }) +}) \ No newline at end of file diff --git a/test/listen/dns.test.js b/test/listen/dns.test.js index a078aa0..6ef322f 100644 --- a/test/listen/dns.test.js +++ b/test/listen/dns.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const dns = require("node:dns"); const { createServer } = require("../../lib"); diff --git a/test/listen/error.test.js b/test/listen/error.test.js index 1ed02a9..bf8079c 100644 --- a/test/listen/error.test.js +++ b/test/listen/error.test.js @@ -1,7 +1,10 @@ +'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" }); @@ -150,4 +153,28 @@ describe("error", () => { 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 index b636d06..ed9b8c9 100644 --- a/test/listen/host.test.js +++ b/test/listen/host.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); diff --git a/test/listen/listen-after-close.test.js b/test/listen/listen-after-close.test.js index c9e0d78..f0e037d 100644 --- a/test/listen/listen-after-close.test.js +++ b/test/listen/listen-after-close.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); diff --git a/test/listen/normalize.test.js b/test/listen/normalize.test.js new file mode 100644 index 0000000..69d35ac --- /dev/null +++ b/test/listen/normalize.test.js @@ -0,0 +1,156 @@ +'use strict' +const { describe, test } = require("node:test"); +const { normalizeListenOptions } = require("../../lib/listen"); +const { Socket } = require("node:net"); +const { inspect } = require("node:util"); + +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) + }) + } +}) \ No newline at end of file diff --git a/test/listen/socket.test.js b/test/listen/socket.test.js index abd90b7..b76c256 100644 --- a/test/listen/socket.test.js +++ b/test/listen/socket.test.js @@ -1,9 +1,11 @@ +'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 { once } = require("node:stream"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -19,7 +21,7 @@ describe("socket", () => { ); try { fs.unlinkSync(sockFile); - } catch {} + } catch { } } else { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } @@ -47,4 +49,22 @@ describe("socket", () => { await promise; }); + + // Refs: https://github.com/fastify/fastify/pull/6937 + test(".listen({ path, host })", { skip: !sockPath }, async (t) => { + t.plan(4); + const { promise, resolve } = withResolvers(); + + 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 index 33794cf..24597d6 100644 --- a/test/listening-origin.test.js +++ b/test/listening-origin.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const dns = require("node:dns"); const os = require("node:os"); @@ -71,7 +72,7 @@ describe("listeningOrigin", () => { ); try { fs.unlinkSync(sockFile); - } catch {} + } catch { } } else { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } @@ -84,4 +85,56 @@ describe("listeningOrigin", () => { 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..d5c5655 --- /dev/null +++ b/test/options/connection-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("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 index cffcd11..df30e57 100644 --- a/test/options/keep-alive-timeout.test.js +++ b/test/options/keep-alive-timeout.test.js @@ -1,3 +1,4 @@ +'use strict' const http = require("node:http"); const { describe, test } = require("node:test"); const { once } = require("node:stream"); @@ -40,6 +41,12 @@ describe("keepAliveTimeout", () => { 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); 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..f8459ae --- /dev/null +++ b/test/options/max-requests-per-socket.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("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 }); + 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..0dd7bc2 --- /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 }); + t.assert.strictEqual(server.requestTimeout, 1); + }); + + test("https", (t) => { + t.plan(1); + const server = createServer({ requestTimeout: 2, https: {} }); + 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 index f9cca68..75b410f 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -1,3 +1,4 @@ +'use strict' const { describe, test } = require("node:test"); const http = require("node:http"); const { createServer } = require("../lib"); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..125528a --- /dev/null +++ b/test/utils.js @@ -0,0 +1,36 @@ +'use strict' +const { hostname, networkInterfaces } = require('node:os') +const { pki, util } = require('node-forge') + + +function buildCertificate() { + if (!global.context || !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 \ No newline at end of file diff --git a/types/server.d.ts b/types/server.d.ts index 689864a..d635fea 100644 --- a/types/server.d.ts +++ b/types/server.d.ts @@ -45,6 +45,8 @@ export interface HTTP2ServerOptions extends CommonServerOptions { http?: NodeHTTP2ServerOptions; https?: false; http2: true; + /** @default 72000 */ + http2SessionTimeout?: number } export type HTTP2RequestListener = NodeRequestListener; export type HTTP2Server = NodeHTTP2Server; From 9af9fe21aabcad5841b1fb68c8c5f6d2e2a205e9 Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 13 Aug 2026 04:45:23 +0800 Subject: [PATCH 05/13] fixup --- lib/index.js | 12 +- lib/listen.js | 9 +- lib/server.js | 30 ++-- lib/utils.js | 38 ++--- test/abort-signal.test.js | 2 +- test/close/double-close.test.js | 22 +-- test/close/http2-session-timeout.test.js | 126 ++++++++-------- test/listen/dns.test.js | 2 +- test/listen/error.test.js | 36 ++--- test/listen/host.test.js | 2 +- test/listen/listen-after-close.test.js | 2 +- test/listen/normalize.test.js | 149 +++++++++---------- test/listen/socket.test.js | 10 +- test/listening-origin.test.js | 4 +- test/options/connection-timeout.test.js | 8 +- test/options/keep-alive-timeout.test.js | 8 +- test/options/max-requests-per-socket.test.js | 8 +- test/options/request-timeout.test.js | 2 +- test/security.test.js | 2 +- test/utils.js | 49 +++--- types/server.d.ts | 2 +- 21 files changed, 266 insertions(+), 257 deletions(-) diff --git a/lib/index.js b/lib/index.js index 75c4b73..050982b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,11 @@ const { selectServer, normalizeServerOptions } = require("./server"); const { listenLookupHosts, normalizeListenOptions } = require("./listen"); -const { normalizeCallback, listeningOrigin, stateClose, syncInternalServers } = require("./utils"); +const { + normalizeCallback, + listeningOrigin, + stateClose, + syncInternalServers, +} = require("./utils"); const { kState, kRaw, kInternalServers } = require("./symbols"); const { ERR_SERVER_ALREADY_LISTEN } = require("./errors"); @@ -85,11 +90,12 @@ function createServer(options, requestListener) { if (name === "listen") return listen; if (name === "close") return close; if (name === "addresses") return addresses; - if (name === "listeningOrigin") return listeningOrigin(server, serverOptions); + if (name === "listeningOrigin") + return listeningOrigin(server, serverOptions); return Reflect.get(target, name, receiver); }, set(target, name, value, receiver) { - syncInternalServers(type, internalServers, name, value) + syncInternalServers(type, internalServers, name, value); // always passthrough in last return Reflect.set(target, name, value, receiver); }, diff --git a/lib/listen.js b/lib/listen.js index f274e0c..b19be7e 100644 --- a/lib/listen.js +++ b/lib/listen.js @@ -18,13 +18,13 @@ function normalizeListenOptions(options = {}) { // port take precedence over unix-socket listenOptions.port = options.port; listenOptions.host = options.host ?? "localhost"; - } else if (typeof options.path === 'string') { + } 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' + listenOptions.host = options.host ?? "localhost"; } // copy other options @@ -148,7 +148,10 @@ function listenLookupHosts( port: mainAddress.port, }; - const { server: newServer } = selectServer(serverOptions, requestHandler); + const { server: newServer } = selectServer( + serverOptions, + requestHandler, + ); newServer.on("error", onError); newServer.on("listening", onListening); diff --git a/lib/server.js b/lib/server.js index 7f386c0..4a38626 100644 --- a/lib/server.js +++ b/lib/server.js @@ -25,41 +25,35 @@ function normalizeServerOptions(options) { Number.isInteger(options.keepAliveTimeout) ) serverOptions.keepAliveTimeout = options.keepAliveTimeout; - else - serverOptions.keepAliveTimeout = 72000; + else serverOptions.keepAliveTimeout = 72000; if ( typeof options.connectionTimeout === "number" && Number.isInteger(options.connectionTimeout) ) serverOptions.connectionTimeout = options.connectionTimeout; - else - serverOptions.connectionTimeout = 0; + else serverOptions.connectionTimeout = 0; if ( typeof options.maxRequestsPerSocket === "number" && Number.isInteger(options.maxRequestsPerSocket) ) serverOptions.maxRequestsPerSocket = options.maxRequestsPerSocket; - else - serverOptions.maxRequestsPerSocket = 0; + else serverOptions.maxRequestsPerSocket = 0; if ( typeof options.requestTimeout === "number" && Number.isInteger(options.requestTimeout) ) serverOptions.requestTimeout = options.requestTimeout; - else - serverOptions.requestTimeout = 0; + else serverOptions.requestTimeout = 0; if ( typeof options.http2SessionTimeout === "number" && Number.isInteger(options.http2SessionTimeout) ) serverOptions.http2SessionTimeout = options.http2SessionTimeout; - else - serverOptions.http2SessionTimeout = 72000; - + else serverOptions.http2SessionTimeout = 72000; return serverOptions; } @@ -247,19 +241,19 @@ function createCustomServer(options, requestListener) { * @returns {Server} */ function selectServer(options, requestListener) { - const result = { type: '', server: null } + const result = { type: "", server: null }; if (isCustom(options)) { - result.type = 'custom' - result.server = createCustomServer(options, requestListener) + result.type = "custom"; + result.server = createCustomServer(options, requestListener); } else if (isHTTP2(options)) { - result.type = 'http2' - result.server = createHTTP2Server(options, requestListener) + result.type = "http2"; + result.server = createHTTP2Server(options, requestListener); } else if (isHTTPS(options)) { - result.type = 'https' + result.type = "https"; result.server = createHTTPSServer(options, requestListener); } else { - result.type = 'http' + result.type = "http"; result.server = createHTTPServer(options, requestListener); } diff --git a/lib/utils.js b/lib/utils.js index 89ce38d..b54cfe2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -49,35 +49,35 @@ function stateClose(state, internalServers) { } 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 -]) + "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 -]) + "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 -]) + "timeout", // https://nodejs.org/docs/latest-v26.x/api/http2.html#servertimeout +]); const commonProperties = { http: httpProperties, - https: httpProperties, // https.Server extends http.Server + https: httpProperties, // https.Server extends http.Server http2: http2Properties, - custom: new Set() // do not sync for custom server -} + custom: new Set(), // do not sync for custom server +}; function syncInternalServers(type, servers, name, value) { - const properites = commonProperties[type] + const properites = commonProperties[type]; if (properites.has(name)) { for (const server of servers) { - server[name] = value + server[name] = value; } } } @@ -87,5 +87,5 @@ module.exports = { withResolvers, listeningOrigin, stateClose, - syncInternalServers + syncInternalServers, }; diff --git a/test/abort-signal.test.js b/test/abort-signal.test.js index 449f170..d216bc3 100644 --- a/test/abort-signal.test.js +++ b/test/abort-signal.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const { createServer } = require("../lib"); const { once } = require("node:stream"); diff --git a/test/close/double-close.test.js b/test/close/double-close.test.js index 18b382e..76c2877 100644 --- a/test/close/double-close.test.js +++ b/test/close/double-close.test.js @@ -6,14 +6,14 @@ const handler = (_request, response) => { 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) - }) -}) \ No newline at end of file +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 index 76fc1bf..f523626 100644 --- a/test/close/http2-session-timeout.test.js +++ b/test/close/http2-session-timeout.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test, before } = require("node:test"); const { createServer } = require("../../lib/index"); const { connect, constants } = require("node:http2"); @@ -6,86 +6,84 @@ const { once } = require("node:stream"); const { buildCertificate } = require("../utils"); const { withResolvers } = require("../../lib/utils"); -const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); -}; +describe("http2SessionTimeout", () => { + before(buildCertificate); -describe('http2SessionTimeout', () => { - before(buildCertificate) - - test('http2 close with async-await', async (t) => { + 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() - }) + 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 (t) => { + test("http2s close with async-await", async () => { const server = createServer({ http2: true, https: { key: global.context.privateKey, - cert: global.context.certificate + 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() - }) + 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) + 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, headers, flags) => { - stream.session.once('timeout', resolve) + 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', + [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() - }) + 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) + 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) - }) -}) \ No newline at end of file + 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 index 6ef322f..4aea64c 100644 --- a/test/listen/dns.test.js +++ b/test/listen/dns.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const dns = require("node:dns"); const { createServer } = require("../../lib"); diff --git a/test/listen/error.test.js b/test/listen/error.test.js index bf8079c..289b2b5 100644 --- a/test/listen/error.test.js +++ b/test/listen/error.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const { once } = require("node:stream"); const http = require("node:http"); @@ -154,27 +154,27 @@ describe("error", () => { }); }); - test('second server listen error', async (t) => { - t.plan(1) + 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) + 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() - }) + server.emit("error", new Error("kaboom!")); + server.close(); + }); } - count++ - return server - }) + count++; + return server; + }); const server = createServer({}, handler); - await server.listen() - t.assert.strictEqual(server.addresses().length, 1) - await server.close() - mock.restore() - }) + 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 index ed9b8c9..edc5b25 100644 --- a/test/listen/host.test.js +++ b/test/listen/host.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); diff --git a/test/listen/listen-after-close.test.js b/test/listen/listen-after-close.test.js index f0e037d..99fe675 100644 --- a/test/listen/listen-after-close.test.js +++ b/test/listen/listen-after-close.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); diff --git a/test/listen/normalize.test.js b/test/listen/normalize.test.js index 69d35ac..7d35c96 100644 --- a/test/listen/normalize.test.js +++ b/test/listen/normalize.test.js @@ -1,156 +1,155 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const { normalizeListenOptions } = require("../../lib/listen"); const { Socket } = require("node:net"); -const { inspect } = require("node:util"); -describe('listen options normalize', () => { - const handle = Socket() - const abort = new AbortController() +describe("listen options normalize", () => { + const handle = Socket(); + const abort = new AbortController(); const cases = [ { - name: '{ handle }', + name: "{ handle }", actual: { handle }, - expect: { handle } + expect: { handle }, }, { - name: '{ handle, path }', - actual: { handle, path: '' }, - 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 }", + 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: "{ handle, host, port }", + actual: { handle, host: "127.0.0.1", port: 1 }, + expect: { handle }, }, { - name: '{ path }', - actual: { path: '' }, - expect: { path: '' } + name: "{ path }", + actual: { path: "" }, + expect: { path: "" }, }, { - name: '{ path, host }', - actual: { path: '', host: '127.0.0.1' }, - 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, 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: "{ path, host, port }", + actual: { path: "", host: "127.0.0.1", port: 1 }, + expect: { host: "127.0.0.1", port: 1 }, }, { - name: '{ port }', + name: "{ port }", actual: { port: 1 }, - expect: { host: 'localhost', 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 }", + 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: "{ host, port }", + actual: { host: "127.0.0.1", port: 1 }, + expect: { host: "127.0.0.1", port: 1 }, }, { - name: '{ backlog: 1 }', + name: "{ backlog: 1 }", actual: { backlog: 1 }, - expect: { host: 'localhost', port: 0, backlog: 1 } + expect: { host: "localhost", port: 0, backlog: 1 }, }, { - name: '{ backlog: true }', + name: "{ backlog: true }", actual: { backlog: true }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ exclusive: 1 }', + name: "{ exclusive: 1 }", actual: { exclusive: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ exclusive: true }', + name: "{ exclusive: true }", actual: { exclusive: true }, - expect: { host: 'localhost', port: 0, exclusive: true } + expect: { host: "localhost", port: 0, exclusive: true }, }, { - name: '{ ipv6Only: 1 }', + name: "{ ipv6Only: 1 }", actual: { ipv6Only: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ ipv6Only: true }', + name: "{ ipv6Only: true }", actual: { ipv6Only: true }, - expect: { host: 'localhost', port: 0, ipv6Only: true } + expect: { host: "localhost", port: 0, ipv6Only: true }, }, { - name: '{ reusePort: 1 }', + name: "{ reusePort: 1 }", actual: { reusePort: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ reusePort: true }', + name: "{ reusePort: true }", actual: { reusePort: true }, - expect: { host: 'localhost', port: 0, reusePort: true } + expect: { host: "localhost", port: 0, reusePort: true }, }, { - name: '{ readableAll: 1 }', + name: "{ readableAll: 1 }", actual: { readableAll: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ readableAll: true }', + name: "{ readableAll: true }", actual: { readableAll: true }, - expect: { host: 'localhost', port: 0, readableAll: true } + expect: { host: "localhost", port: 0, readableAll: true }, }, { - name: '{ writableAll: 1 }', + name: "{ writableAll: 1 }", actual: { writableAll: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ writableAll: true }', + name: "{ writableAll: true }", actual: { writableAll: true }, - expect: { host: 'localhost', port: 0, writableAll: true } + expect: { host: "localhost", port: 0, writableAll: true }, }, { - name: '{ signal: 1 }', + name: "{ signal: 1 }", actual: { signal: 1 }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ signal: {} }', + name: "{ signal: {} }", actual: { signal: {} }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ signal: null }', + name: "{ signal: null }", actual: { signal: null }, - expect: { host: 'localhost', port: 0 } + expect: { host: "localhost", port: 0 }, }, { - name: '{ signal }', + name: "{ signal }", actual: { signal: abort.signal }, - expect: { host: 'localhost', port: 0, 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) - }) + t.plan(1); + const result = normalizeListenOptions(actual); + t.assert.deepStrictEqual(result, expect); + }); } -}) \ No newline at end of file +}); diff --git a/test/listen/socket.test.js b/test/listen/socket.test.js index b76c256..707920c 100644 --- a/test/listen/socket.test.js +++ b/test/listen/socket.test.js @@ -1,11 +1,10 @@ -'use strict' +"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 { once } = require("node:stream"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -21,7 +20,7 @@ describe("socket", () => { ); try { fs.unlinkSync(sockFile); - } catch { } + } catch {} } else { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } @@ -53,12 +52,11 @@ describe("socket", () => { // Refs: https://github.com/fastify/fastify/pull/6937 test(".listen({ path, host })", { skip: !sockPath }, async (t) => { t.plan(4); - const { promise, resolve } = withResolvers(); const server = createServer({}, handler); t.assert.strictEqual(server.listening, false); - await server.listen({ path: sockPath, host: 'localhost' }); + await server.listen({ path: sockPath, host: "localhost" }); t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); @@ -66,5 +64,5 @@ describe("socket", () => { t.assert.deepStrictEqual(addresses, [sockPath]); await server.close(); - }) + }); }); diff --git a/test/listening-origin.test.js b/test/listening-origin.test.js index 24597d6..262282e 100644 --- a/test/listening-origin.test.js +++ b/test/listening-origin.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const dns = require("node:dns"); const os = require("node:os"); @@ -72,7 +72,7 @@ describe("listeningOrigin", () => { ); try { fs.unlinkSync(sockFile); - } catch { } + } catch {} } else { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } diff --git a/test/options/connection-timeout.test.js b/test/options/connection-timeout.test.js index d5c5655..99f9ae3 100644 --- a/test/options/connection-timeout.test.js +++ b/test/options/connection-timeout.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const http = require("node:http"); const { describe, test } = require("node:test"); const { once } = require("node:stream"); @@ -43,7 +43,11 @@ describe("connectionTimeout", () => { test("http2 + https", (t) => { t.plan(1); - const server = createServer({ connectionTimeout: 3, http2: true, https: {} }); + const server = createServer({ + connectionTimeout: 3, + http2: true, + https: {}, + }); t.assert.strictEqual(server.timeout, 3); }); diff --git a/test/options/keep-alive-timeout.test.js b/test/options/keep-alive-timeout.test.js index df30e57..616b75c 100644 --- a/test/options/keep-alive-timeout.test.js +++ b/test/options/keep-alive-timeout.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const http = require("node:http"); const { describe, test } = require("node:test"); const { once } = require("node:stream"); @@ -43,7 +43,11 @@ describe("keepAliveTimeout", () => { test("http2 + https", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: 3, http2: true, https: {} }); + const server = createServer({ + keepAliveTimeout: 3, + http2: true, + https: {}, + }); t.assert.notStrictEqual(server.keepAliveTimeout, 3); }); diff --git a/test/options/max-requests-per-socket.test.js b/test/options/max-requests-per-socket.test.js index f8459ae..f60a3fe 100644 --- a/test/options/max-requests-per-socket.test.js +++ b/test/options/max-requests-per-socket.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const http = require("node:http"); const { describe, test } = require("node:test"); const { once } = require("node:stream"); @@ -43,7 +43,11 @@ describe("maxRequestsPerSocket", () => { test("http2 + https", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 3, http2: true, https: {} }); + const server = createServer({ + maxRequestsPerSocket: 3, + http2: true, + https: {}, + }); t.assert.notStrictEqual(server.maxRequestsPerSocket, 3); }); diff --git a/test/options/request-timeout.test.js b/test/options/request-timeout.test.js index 0dd7bc2..84bb38e 100644 --- a/test/options/request-timeout.test.js +++ b/test/options/request-timeout.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const http = require("node:http"); const { describe, test } = require("node:test"); const { once } = require("node:stream"); diff --git a/test/security.test.js b/test/security.test.js index 75b410f..75bb722 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -1,4 +1,4 @@ -'use strict' +"use strict"; const { describe, test } = require("node:test"); const http = require("node:http"); const { createServer } = require("../lib"); diff --git a/test/utils.js b/test/utils.js index 125528a..ac8b4d2 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,36 +1,35 @@ -'use strict' -const { hostname, networkInterfaces } = require('node:os') -const { pki, util } = require('node-forge') - +"use strict"; +const { hostname } = require("node:os"); +const { pki } = require("node-forge"); function buildCertificate() { - if (!global.context || !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) + 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) + { 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) + certificate.sign(privateKey); global.context = { privateKey: pki.privateKeyToPem(privateKey), publicKey: pki.publicKeyToPem(publicKey), - certificate: pki.certificateToPem(certificate) - } + certificate: pki.certificateToPem(certificate), + }; } } -module.exports.buildCertificate = buildCertificate \ No newline at end of file +module.exports.buildCertificate = buildCertificate; diff --git a/types/server.d.ts b/types/server.d.ts index d635fea..93323cd 100644 --- a/types/server.d.ts +++ b/types/server.d.ts @@ -46,7 +46,7 @@ export interface HTTP2ServerOptions extends CommonServerOptions { https?: false; http2: true; /** @default 72000 */ - http2SessionTimeout?: number + http2SessionTimeout?: number; } export type HTTP2RequestListener = NodeRequestListener; export type HTTP2Server = NodeHTTP2Server; From c1132051509a281eb884a66620ffed322a790ed3 Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 13 Aug 2026 04:46:50 +0800 Subject: [PATCH 06/13] fixup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4a99a3..de74ecb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "scripts": { "test": "npm run test:unit && npm run test:types", - "test:unit": "node --test --test-timeout=500", + "test:unit": "node --test --test-timeout=2000", "test:coverage": "c8 npm run test:unit", "test:types": "tstyche", "lint": "npx @biomejs/biome check", From db49feac9ce6dfba0d11dc5ab49ac338a4614148 Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 13 Aug 2026 04:50:30 +0800 Subject: [PATCH 07/13] fixup --- .github/dependabot.yml | 4 ++++ .github/workflows/ci.yml | 3 ++- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e7c1a63..c909645 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,6 +34,8 @@ updates: # Production dependencies with breaking changes dependencies: dependency-type: "production" + patterns: + - "*" # TypeScript related dependencies dev-dependencies-typescript: patterns: @@ -43,3 +45,5 @@ updates: # Development dependencies with breaking changes dev-dependencies: dependency-type: "development" + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64ad0bf..dd784e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,8 @@ jobs: permissions: contents: write pull-requests: write - uses: fastify/workflows/.github/workflows/plugins-ci.yml@2073dc8e1f9e172bf42daa3843c9dbd31af1e8cb # v6.0.0 + 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/package.json b/package.json index de74ecb..545a0b5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "scripts": { "test": "npm run test:unit && npm run test:types", - "test:unit": "node --test --test-timeout=2000", + "test:unit": "node --test", "test:coverage": "c8 npm run test:unit", "test:types": "tstyche", "lint": "npx @biomejs/biome check", From 83fe355ccf6b1647934d1f72df81880d937489e0 Mon Sep 17 00:00:00 2001 From: KaKa Date: Thu, 13 Aug 2026 04:53:43 +0800 Subject: [PATCH 08/13] fixup --- lib/server.js | 4 +--- package.json | 2 +- test/options/max-requests-per-socket.test.js | 2 +- test/options/request-timeout.test.js | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/server.js b/lib/server.js index 4a38626..a78b630 100644 --- a/lib/server.js +++ b/lib/server.js @@ -104,9 +104,7 @@ function isHTTPS(options) { function createHTTPSServer(options, requestListener) { const https = require("node:https"); - const serverOptions = options.https === true ? {} : options.https; - - const server = https.createServer(serverOptions, requestListener); + const server = https.createServer(options.https, requestListener); server.keepAliveTimeout = options.keepAliveTimeout; server.requestTimeout = options.requestTimeout; diff --git a/package.json b/package.json index 545a0b5..83b605e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "test": "npm run test:unit && npm run test:types", "test:unit": "node --test", - "test:coverage": "c8 npm run test:unit", + "test:coverage": "c8 --100 npm run test:unit", "test:types": "tstyche", "lint": "npx @biomejs/biome check", "lint:fix": "npx @biomejs/biome check --write" diff --git a/test/options/max-requests-per-socket.test.js b/test/options/max-requests-per-socket.test.js index f60a3fe..587788d 100644 --- a/test/options/max-requests-per-socket.test.js +++ b/test/options/max-requests-per-socket.test.js @@ -25,7 +25,7 @@ describe("maxRequestsPerSocket", () => { test("http", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 1 }); + const server = createServer({ maxRequestsPerSocket: 1, http: {} }); t.assert.strictEqual(server.maxRequestsPerSocket, 1); }); diff --git a/test/options/request-timeout.test.js b/test/options/request-timeout.test.js index 84bb38e..b632f68 100644 --- a/test/options/request-timeout.test.js +++ b/test/options/request-timeout.test.js @@ -25,13 +25,13 @@ describe("requestTimeout", () => { test("http", (t) => { t.plan(1); - const server = createServer({ requestTimeout: 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: {} }); + const server = createServer({ requestTimeout: 2, https: true }); t.assert.strictEqual(server.requestTimeout, 2); }); From ab32d649ca7111fabc39e16d8158682258e34864 Mon Sep 17 00:00:00 2001 From: KaKa Date: Sat, 15 Aug 2026 18:01:00 +0800 Subject: [PATCH 09/13] test: fix ubuntu localhost resolve --- biome.json | 3 +- test/listen/dns.test.js | 7 +++-- test/listen/error.test.js | 13 +++++---- test/listen/host.test.js | 19 +++++++------ test/listen/listen-after-close.test.js | 9 ++++-- test/options/connection-timeout.test.js | 7 +++-- test/options/keep-alive-timeout.test.js | 7 +++-- test/options/max-requests-per-socket.test.js | 7 +++-- test/options/request-timeout.test.js | 7 +++-- test/utils.js | 29 ++++++++++++++++---- 10 files changed, 76 insertions(+), 32 deletions(-) diff --git a/biome.json b/biome.json index 0af95fa..7578b8a 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,8 @@ "useIgnoreFile": false }, "files": { - "ignoreUnknown": false + "ignoreUnknown": false, + "includes": ["**", "!!coverage"] }, "formatter": { "enabled": true, diff --git a/test/listen/dns.test.js b/test/listen/dns.test.js index 4aea64c..d8ac4e7 100644 --- a/test/listen/dns.test.js +++ b/test/listen/dns.test.js @@ -100,7 +100,7 @@ describe("dns", () => { (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) { + if (hostname === "localhost") { callback(null, [ { address: "::1", family: 6 }, { address: "127.0.0.1", family: 4 }, @@ -120,7 +120,10 @@ describe("dns", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 3); + // Linux (Ubuntu) with SO_REUSEADDR off will treats + // 0.0.0.0 and 127.0.0.1 as the same host + // so, we check for >= 2 + t.assert.strictEqual(addresses.length >= 2, true); server.close(); }); server.once("fastify.close", () => { diff --git a/test/listen/error.test.js b/test/listen/error.test.js index 289b2b5..346b0b3 100644 --- a/test/listen/error.test.js +++ b/test/listen/error.test.js @@ -1,10 +1,11 @@ "use strict"; -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { once } = require("node:stream"); const http = require("node:http"); +const { nextTick } = require("node:process"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); -const { nextTick } = require("node:process"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -12,6 +13,8 @@ const handler = (_request, response) => { }; describe("error", () => { + before(localhostCount); + describe("ERR_SERVER_ALREADY_LISTEN", () => { test(".listen()", async (t) => { t.plan(5); @@ -28,7 +31,7 @@ describe("error", () => { server.listen({}, (error) => { t.assert.ifError(error); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.listen({}, (error) => { t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN"); server.close(); @@ -71,7 +74,7 @@ describe("error", () => { await server.listen(); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); try { await server.listen(); @@ -112,7 +115,7 @@ describe("error", () => { server.listen({}, (error, address) => { t.assert.ifError(error); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); conflict.listen({ port: address.port }, (error) => { t.assert.strictEqual(error.code, "EADDRINUSE"); diff --git a/test/listen/host.test.js b/test/listen/host.test.js index edc5b25..42e7a46 100644 --- a/test/listen/host.test.js +++ b/test/listen/host.test.js @@ -1,7 +1,8 @@ "use strict"; -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -9,6 +10,8 @@ const handler = (_request, response) => { }; describe("host/port", () => { + before(localhostCount); + test(".listen()", async (t) => { t.plan(4); const { promise, resolve } = withResolvers(); @@ -19,7 +22,7 @@ describe("host/port", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); server.once("fastify.close", () => { @@ -42,7 +45,7 @@ describe("host/port", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); server.once("fastify.close", () => { @@ -65,7 +68,7 @@ describe("host/port", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); server.once("fastify.close", () => { @@ -134,7 +137,7 @@ describe("host/port", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); server.once("fastify.close", () => { @@ -157,7 +160,7 @@ describe("host/port", () => { server.once("fastify.listening", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); server.once("fastify.close", () => { @@ -196,7 +199,7 @@ describe("host/port", () => { t.assert.ifError(error); t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }, ); @@ -220,7 +223,7 @@ describe("host/port", () => { t.assert.ifError(error); t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(); }); diff --git a/test/listen/listen-after-close.test.js b/test/listen/listen-after-close.test.js index 99fe675..ed6926e 100644 --- a/test/listen/listen-after-close.test.js +++ b/test/listen/listen-after-close.test.js @@ -1,7 +1,8 @@ "use strict"; -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { createServer } = require("../../lib"); const { withResolvers } = require("../../lib/utils"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -9,6 +10,8 @@ const handler = (_request, response) => { }; describe("listen again after close", () => { + before(localhostCount); + test(".listen()", async (t) => { t.plan(6); const { promise, resolve } = withResolvers(); @@ -19,7 +22,7 @@ describe("listen again after close", () => { server.listen({}, (error) => { t.assert.ifError(error); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(() => { t.assert.strictEqual(server.listening, false); @@ -27,7 +30,7 @@ describe("listen again after close", () => { server.listen({}, (error) => { t.assert.ifError(error); const addresses = server.addresses(); - t.assert.strictEqual(addresses.length, 2); + t.assert.strictEqual(addresses.length, global.context.localhostCount); server.close(resolve); }); }); diff --git a/test/options/connection-timeout.test.js b/test/options/connection-timeout.test.js index 99f9ae3..356c393 100644 --- a/test/options/connection-timeout.test.js +++ b/test/options/connection-timeout.test.js @@ -1,9 +1,10 @@ "use strict"; const http = require("node:http"); -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { once } = require("node:stream"); const { createServer } = require("../../lib"); const { kInternalServers } = require("../../lib/symbols"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -11,6 +12,8 @@ const handler = (_request, response) => { }; describe("connectionTimeout", () => { + before(localhostCount); + test("[]", (t) => { t.plan(1); const server = createServer({ connectionTimeout: [] }); @@ -67,7 +70,7 @@ describe("connectionTimeout", () => { }); test("update all servers", async (t) => { - t.plan(3); + t.plan(1 + global.context.localhostCount); const server = createServer({ connectionTimeout: 1 }, handler); t.assert.strictEqual(server.timeout, 1); server.listen(); diff --git a/test/options/keep-alive-timeout.test.js b/test/options/keep-alive-timeout.test.js index 616b75c..1554793 100644 --- a/test/options/keep-alive-timeout.test.js +++ b/test/options/keep-alive-timeout.test.js @@ -1,9 +1,10 @@ "use strict"; const http = require("node:http"); -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { once } = require("node:stream"); const { createServer } = require("../../lib"); const { kInternalServers } = require("../../lib/symbols"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -11,6 +12,8 @@ const handler = (_request, response) => { }; describe("keepAliveTimeout", () => { + before(localhostCount); + test("[]", (t) => { t.plan(1); const server = createServer({ keepAliveTimeout: [] }); @@ -67,7 +70,7 @@ describe("keepAliveTimeout", () => { }); test("update all servers", async (t) => { - t.plan(3); + t.plan(1 + global.context.localhostCount); const server = createServer({ keepAliveTimeout: 1 }, handler); t.assert.strictEqual(server.keepAliveTimeout, 1); server.listen(); diff --git a/test/options/max-requests-per-socket.test.js b/test/options/max-requests-per-socket.test.js index 587788d..77c3229 100644 --- a/test/options/max-requests-per-socket.test.js +++ b/test/options/max-requests-per-socket.test.js @@ -1,9 +1,10 @@ "use strict"; const http = require("node:http"); -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { once } = require("node:stream"); const { createServer } = require("../../lib"); const { kInternalServers } = require("../../lib/symbols"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -11,6 +12,8 @@ const handler = (_request, response) => { }; describe("maxRequestsPerSocket", () => { + before(localhostCount); + test("[]", (t) => { t.plan(1); const server = createServer({ maxRequestsPerSocket: [] }); @@ -67,7 +70,7 @@ describe("maxRequestsPerSocket", () => { }); test("update all servers", async (t) => { - t.plan(3); + t.plan(1 + global.context.localhostCount); const server = createServer({ maxRequestsPerSocket: 1 }, handler); t.assert.strictEqual(server.maxRequestsPerSocket, 1); server.listen(); diff --git a/test/options/request-timeout.test.js b/test/options/request-timeout.test.js index b632f68..844d441 100644 --- a/test/options/request-timeout.test.js +++ b/test/options/request-timeout.test.js @@ -1,9 +1,10 @@ "use strict"; const http = require("node:http"); -const { describe, test } = require("node:test"); +const { describe, test, before } = require("node:test"); const { once } = require("node:stream"); const { createServer } = require("../../lib"); const { kInternalServers } = require("../../lib/symbols"); +const { localhostCount } = require("../utils"); const handler = (_request, response) => { response.writeHead(200, { "Content-Type": "application/json" }); @@ -11,6 +12,8 @@ const handler = (_request, response) => { }; describe("requestTimeout", () => { + before(localhostCount); + test("[]", (t) => { t.plan(1); const server = createServer({ requestTimeout: [] }); @@ -63,7 +66,7 @@ describe("requestTimeout", () => { }); test("update all servers", async (t) => { - t.plan(3); + t.plan(1 + global.context.localhostCount); const server = createServer({ requestTimeout: 1 }, handler); t.assert.strictEqual(server.requestTimeout, 1); server.listen(); diff --git a/test/utils.js b/test/utils.js index ac8b4d2..882ed3b 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,5 +1,6 @@ "use strict"; const { hostname } = require("node:os"); +const { lookup } = require("node:dns"); const { pki } = require("node-forge"); function buildCertificate() { @@ -24,12 +25,30 @@ function buildCertificate() { certificate.sign(privateKey); - global.context = { - privateKey: pki.privateKeyToPem(privateKey), - publicKey: pki.publicKeyToPem(publicKey), - certificate: pki.certificateToPem(certificate), - }; + global.context ??= {}; + global.context.privateKey = pki.privateKeyToPem(privateKey); + global.context.publicKey = pki.publicKeyToPem(publicKey); + global.context.certificate = pki.certificateToPem(certificate); } } +// same system do not enable IPv6 or do not return IPv6 for localhost +// even when we specified `{ all: true }`. +// we need the localhost count for those system to ensure test assert +// correctly. +function localhostCount(_, done) { + if (!global.context?.localhostCount) { + lookup("localhost", { all: true }, (error, addresses) => { + if (error) { + done(error); + } else { + global.context ??= {}; + global.context.localhostCount = addresses.length; + done(); + } + }); + } +} + +module.exports.localhostCount = localhostCount; module.exports.buildCertificate = buildCertificate; From b4eb5915a3ee090844a539742f9f6f9666c006b6 Mon Sep 17 00:00:00 2001 From: KaKa Date: Sat, 15 Aug 2026 19:27:26 +0800 Subject: [PATCH 10/13] docs: add API --- README.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2ba2498..6e400be 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ npm install @fastify/server This example show the basic usage of create web server. -```typescript +```TypeScript import { createServer } from '@fastify/server' const server = createServer({}, function(request, response) { @@ -22,7 +22,176 @@ server.listen({ port: 3000 }) ## API -## Difference from `node:http`, `node:https` and `node:http2`. +### createServer([options[, requestHandler]]): Server + +#### `options.http` + +Accept either `true` or the same options passed to [`http.createServer`](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener). When combine with `options.http2 = true` it accepts +[`http2.createServer`](https://nodejs.org/docs/latest/api/http2.html#http2createserveroptions-onrequesthandler) instead. + +#### `options.https` + +Accept either `true` or the same options passed to [`https.createServer`](https://nodejs.org/docs/latest/api/https.html#httpscreateserveroptions-requestlistener). When combine with +`options.http2 = true` it accepts [`http2.createSecureServer`](https://nodejs.org/docs/latest/api/http2.html#http2createsecureserveroptions-onrequesthandler) instead. + +#### `options.http2` + +Passing `true` to create `node:http2` server, the options for `createServer` will be passed by either `options.http` or `options.https`. + +#### `options.serverFactory(requestListener[, options])` + +Accept `function` that return server created by `node:http`, `node:https` or `node:http2`. +It will pass through the `options` and `requestListener` for the server creation. + +```TypeScript +import http from 'node:http' +import { createServer } from '@fastify/server' + +function serverFactory(requestListener, options) { + // options is the same as + // first argument passed to createServer + return http.createServer(options.http, requestListener) +} + +const server = createServer({ + http: {}, + serverFactory +}) +server.listen() +``` + +#### `options.keepAliveTimeout` + +Default: 72000 (72 seconds) + +Accept any positive integer which used to update the `node:http` and `node:https` keepAliveTimeout. + +#### `options.connectionTimeout` + +Default: 0 + +Accept any positive integer which used to update the `node:http`, +`node:https` and `node:http2` timeout. + +#### `options.requestTimeout` + +Default: 0 + +Accept any positive integer which used to update the `node:http` +and `node:https` requestTimeout. + +#### `options.maxRequestsPerSocket` + +Default: 0 + +Accept any positive integer which used to update the `node:http` +and `node:https` maxRequestsPerSocket. + +#### `options.http2SessionTimeout` + +Default: 72000 (72 seconds) + +Accept any positive integer which used to update the `node:http2` +session timeout. + +#### `requestHandler(request, response)` + +The `function` that used to attach `request` event for the `node:http`, `node:https` and `node:http2` server. When, using with `node:http2` it should set `allowHTTP1: true` for using the [compatibity feature](https://nodejs.org/docs/latest/api/http2.html#compatibility-api). + +### Server + +The proxied server returned by `createServer`. +It helps to manage multiple server togather and update the properties. + +#### `.listen([options[, callback]])` + +Accept the same `options` passed to [`net.Server.listen`](https://nodejs.org/docs/latest/api/net.html#serverlistenoptions-callback). +However, to ensure the security `host` will be default to `localhost` and `port` will be default to `0` if not specified. +The listen order prority is the same as `Node.js`, starting from `handle > port > path`. + +The `callback` function will be fired once all server is available. +Same as attaching to `fastify.listening` event. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +server.listen({}, function(error, address) { + // address is the main server address +}) +``` + +When `callback` is not specified, the function will return `Promise` +which resolved when all servers are available. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +const address = await server.listen({}) +``` + +#### `.close([callback])` + +Used to close all the server. + +`callback` will be fired once all servers are closed. +Same as attaching to `fastify.close` event. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +await server.listen() +server.close(function(error) { +}) +``` + +When `callback` is not specified, the function will return `Promise` +which resolved when all servers are close. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +await server.listen() +await server.close() +``` + +#### `.addresses()` + +Return all the listening address. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +await server.listen() +const addresses = server.addresses() +for(const address of addresses) { + if(typeof address === 'string') { + // unix-socket / Windows pipe + } else { + console.log(address.address) + console.log(address.family) + } +} +``` + +#### `.listeningOrigin` + +Return URL that allows to connect main server. + +```TypeScript +import { createServer } from '@fastify/server' + +const server = createServer() +await server.listen() + +const response = await fetch(server.listeningOrigin) +``` + +## Difference from `node:http`, `node:https` and `node:http2` ### All-in-one `createServer` From 0368d5152a6c22e0c6ac4053664e24d90e367977 Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 19 Aug 2026 10:33:17 +0800 Subject: [PATCH 11/13] feat: closeAllConnections --- .vscode/settings.json | 7 + README.md | 8 + biome.json | 34 ++- lib/close.js | 55 ++++ lib/errors.js | 1 + lib/index.js | 46 +--- lib/listen.js | 27 +- lib/server.js | 20 +- lib/symbols.js | 13 +- lib/utils.js | 15 +- package.json | 64 ++--- test/abort-signal.test.js | 32 ++- test/close/double-close.test.js | 10 +- test/close/http2-session-timeout.test.js | 11 +- test/listen/dns.test.js | 25 +- test/listen/error.test.js | 66 +++-- test/listen/host.test.js | 32 ++- test/listen/listen-after-close.test.js | 46 ++-- test/listen/normalize.test.js | 253 +++++++++++++++---- test/listen/socket.test.js | 35 ++- test/listening-origin.test.js | 60 ++++- test/options/connection-timeout.test.js | 44 +++- test/options/force-close-connections.test.js | 117 +++++++++ test/options/keep-alive-timeout.test.js | 46 +++- test/options/max-requests-per-socket.test.js | 47 +++- test/options/request-timeout.test.js | 51 +++- test/security.test.js | 10 +- test/utils.js | 54 ++-- tsconfig.json | 12 +- types/errors.d.ts | 4 +- types/index.tst.ts | 35 ++- types/server.d.ts | 2 + 32 files changed, 988 insertions(+), 294 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 lib/close.js create mode 100644 test/options/force-close-connections.test.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ff87b41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "source.action.useSortedPackageJson.biome": "explicit", + "source.fixAll.biome": "explicit" + } +} diff --git a/README.md b/README.md index 6e400be..aabaf12 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,14 @@ Default: 72000 (72 seconds) Accept any positive integer which used to update the `node:http2` session timeout. +#### `options.forceCloseConnections` + +Default: false + +When `true` force close connections when calling `.close`. +Only `node:http` and `node:https` support force closing using `.closeAllConnections`. +`node:http2` will send `GOAWAY` by default and will wait for the session gracefully close. + #### `requestHandler(request, response)` The `function` that used to attach `request` event for the `node:http`, `node:https` and `node:http2` server. When, using with `node:http2` it should set `allowHTTP1: true` for using the [compatibity feature](https://nodejs.org/docs/latest/api/http2.html#compatibility-api). diff --git a/biome.json b/biome.json index 7578b8a..bbae411 100644 --- a/biome.json +++ b/biome.json @@ -7,11 +7,29 @@ }, "files": { "ignoreUnknown": false, - "includes": ["**", "!!coverage"] + "includes": [ + "**", + "!!coverage" + ] + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on", + "useSortedPackageJson": "on" + } + } }, "formatter": { "enabled": true, - "indentStyle": "space" + "expand": "always", + "formatWithErrors": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80, + "trailingNewline": true }, "linter": { "enabled": true, @@ -27,17 +45,11 @@ "quoteStyle": "double" } }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - }, "overrides": [ { - "includes": ["test/security.test.js"], + "includes": [ + "test/security.test.js" + ], "linter": { "rules": { "suspicious": { diff --git a/lib/close.js b/lib/close.js new file mode 100644 index 0000000..15a9065 --- /dev/null +++ b/lib/close.js @@ -0,0 +1,55 @@ +"use strict"; + +const { kInternalServers, kRaw, kState } = require("./symbols"); +const { stateClose } = require("./utils"); + +function closeAllConnections(server, force) { + // only node:http and node:https support force close + // node:http2 by default close idle connection and send GOAWAY when close + if (typeof server.closeAllConnections === "function" && force === true) { + server.closeAllConnections(); + } +} + +function closeAllServers(proxy, force, callback) { + const state = proxy[kState]; + const server = proxy[kRaw]; + const internalServers = proxy[kInternalServers]; + let bound = internalServers.length; + + function onClose() { + server.off("close", onClose); + if (bound === 0) { + stateClose(state, internalServers); + server.emit("fastify.close"); + callback(); + } else { + for (const internalServer of internalServers) { + function internalOnClose() { + internalServer.off("close", internalOnClose); + + bound--; + if (bound === 0) { + stateClose(state, internalServers); + server.emit("fastify.close"); + callback(); + } + } + 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(); + // called only when force close and node:http / node:https + // for node:http2, there is no such thing since session.close also + // wait for the session gracefully closed + closeAllConnections(server, force); +} + +module.exports.closeAllConnections = closeAllConnections; +module.exports.closeAllServers = closeAllServers; diff --git a/lib/errors.js b/lib/errors.js index 55c7f66..a69efb1 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,3 +1,4 @@ +"use strict"; const { createError } = require("@fastify/error"); /** @type {import('../types/errors.d.ts').ERR_SERVER_ALREADY_LISTEN} */ diff --git a/lib/index.js b/lib/index.js index 050982b..cefe2e7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,14 @@ +"use strict"; 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"); +const { closeAllServers } = require("./close"); /** @typedef {import('../types/server').ServerOptions} ServerOptions */ /** @typedef {import('../types/server').HTTPServerOptions} HTTPServerOptions */ @@ -72,12 +73,12 @@ function createServer(options, requestListener) { const serverOptions = normalizeServerOptions(options); const { type, server } = selectServer(serverOptions, requestListener); const state = { - listen: false, - listening: false, + aborted: false, closed: false, closing: false, error: false, - aborted: false, + listen: false, + listening: false, }; // when there are internal servers @@ -119,7 +120,9 @@ function createServer(options, requestListener) { state.aborted = true; close(); }, - { once: true }, + { + once: true, + }, ); } @@ -151,41 +154,16 @@ function createServer(options, requestListener) { 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(); + closeAllServers(proxy, serverOptions.forceCloseConnections, cb); return cb.promise; } function addresses() { if (internalServers.length === 0) { - return [server.address()]; + return [ + server.address(), + ]; } else { const addresses = internalServers.map((server) => server.address()); addresses.unshift(server.address()); diff --git a/lib/listen.js b/lib/listen.js index b19be7e..cc38fdf 100644 --- a/lib/listen.js +++ b/lib/listen.js @@ -1,3 +1,5 @@ +"use strict"; +const { closeAllConnections } = require("./close"); const { selectServer } = require("./server"); const { kRaw, kInternalServers, kState } = require("./symbols"); const { withResolvers } = require("./utils"); @@ -71,13 +73,19 @@ function lookupHosts(listenOptions, state, callback) { 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); - }); + 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); + }, + ); } } @@ -102,8 +110,8 @@ function listenLookupHosts( listenOptions, callback, ) { - server = proxy[kRaw]; - state = proxy[kState]; + const server = proxy[kRaw]; + const state = proxy[kState]; const servers = proxy[kInternalServers]; if (!state.aborted) { @@ -170,6 +178,7 @@ function listenLookupHosts( newServer.off("close", closeServer); server.off("error", closeServer); newServer.close(); + closeAllConnections(newServer, serverOptions.forceCloseConnections); } function cleanup() { diff --git a/lib/server.js b/lib/server.js index a78b630..e7ef9d1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,3 +1,4 @@ +"use strict"; /** @typedef {import('../types/server').ServerOptions} ServerOptions */ /** @@ -55,6 +56,10 @@ function normalizeServerOptions(options) { serverOptions.http2SessionTimeout = options.http2SessionTimeout; else serverOptions.http2SessionTimeout = 72000; + if (typeof options.forceCloseConnections === "boolean") + serverOptions.forceCloseConnections = options.forceCloseConnections; + else serverOptions.forceCloseConnections = false; + return serverOptions; } @@ -163,11 +168,17 @@ function createHTTP2Server(options, requestListener) { } // update options - server.on("session", (session) => { + function cleanup() { + server.off("session", onSession); + server.off("close", cleanup); + } + function onSession(session) { session.setTimeout(options.http2SessionTimeout, () => { session.close(); }); - }); + } + server.on("session", onSession); + server.on("close", cleanup); server.setTimeout(options.connectionTimeout); @@ -239,7 +250,10 @@ function createCustomServer(options, requestListener) { * @returns {Server} */ function selectServer(options, requestListener) { - const result = { type: "", server: null }; + const result = { + server: null, + type: "", + }; if (isCustom(options)) { result.type = "custom"; diff --git a/lib/symbols.js b/lib/symbols.js index d7122f5..c594008 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -1,3 +1,10 @@ -module.exports.kRaw = Symbol("fastify.server.raw"); -module.exports.kState = Symbol("fastify.server.state"); -module.exports.kInternalServers = Symbol("fastify.server.internal.servers"); +"use strict"; +const kRaw = Symbol("fastify.server.raw"); +const kState = Symbol("fastify.server.state"); +const kInternalServers = Symbol("fastify.server.internal.servers"); + +module.exports = { + kInternalServers, + kRaw, + kState, +}; diff --git a/lib/utils.js b/lib/utils.js index b54cfe2..a8bbc02 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,4 @@ +"use strict"; const { isHTTPS } = require("./server"); function normalizeCallback(callback) { @@ -27,7 +28,11 @@ function withResolvers() { res = resolve; rej = reject; }); - return { promise, resolve: res, reject: rej }; + return { + promise, + reject: rej, + resolve: res, + }; } function listeningOrigin(server, options) { @@ -67,10 +72,10 @@ const http2Properties = new Set([ "timeout", // https://nodejs.org/docs/latest-v26.x/api/http2.html#servertimeout ]); const commonProperties = { + custom: new Set(), // do not sync for custom server http: httpProperties, - https: httpProperties, // https.Server extends http.Server http2: http2Properties, - custom: new Set(), // do not sync for custom server + https: httpProperties, // https.Server extends http.Server }; function syncInternalServers(type, servers, name, value) { @@ -83,9 +88,9 @@ function syncInternalServers(type, servers, name, value) { } module.exports = { - normalizeCallback, - withResolvers, listeningOrigin, + normalizeCallback, stateClose, syncInternalServers, + withResolvers, }; diff --git a/package.json b/package.json index 83b605e..308a437 100644 --- a/package.json +++ b/package.json @@ -2,34 +2,14 @@ "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" + "homepage": "https://github.com/fastify/server#readme", + "bugs": { + "url": "https://github.com/fastify/server/issues" }, "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", @@ -40,6 +20,35 @@ "url": "https://opencollective.com/fastify" } ], + "license": "MIT", + "author": { + "name": "KaKa", + "email": "kaka@kakang.dev" + }, + "type": "commonjs", + "main": "./lib/index.js", + "types": "./types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*" + ], + "scripts": { + "lint": "npx @biomejs/biome check", + "lint:fix": "npx @biomejs/biome check --write", + "test": "npm run test:unit && npm run test:types", + "test:coverage": "c8 --100 npm run test:unit", + "test:types": "tstyche", + "test:unit": "node --test" + }, + "c8": { + "reporter": [ + "html", + "text" + ] + }, + "dependencies": { + "@fastify/error": "^4.2.0" + }, "devDependencies": { "@biomejs/biome": "2.5.6", "@types/node": "^26.1.2", @@ -48,16 +57,7 @@ "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 index d216bc3..fb9dc7a 100644 --- a/test/abort-signal.test.js +++ b/test/abort-signal.test.js @@ -4,8 +4,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("abort signal", () => { @@ -15,7 +21,9 @@ describe("abort signal", () => { const controller = new AbortController(); const server = createServer({}, handler); - server.listen({ signal: controller.signal }); + server.listen({ + signal: controller.signal, + }); await once(server, "fastify.listening"); controller.abort(); @@ -37,7 +45,9 @@ describe("abort signal", () => { t.assert.fail("should not reach"); }); - server.listen({ signal: controller.signal }); + server.listen({ + signal: controller.signal, + }); t.assert.strictEqual(server.listening, false); }); @@ -57,7 +67,9 @@ describe("abort signal", () => { t.assert.strictEqual(closeCount, 1); }); - server.listen({ signal: controller.signal }); + server.listen({ + signal: controller.signal, + }); controller.abort(); t.assert.strictEqual(server.listening, false); @@ -78,7 +90,9 @@ describe("abort signal", () => { }); controller.abort(); - server.listen({ signal: controller.signal }); + server.listen({ + signal: controller.signal, + }); t.assert.strictEqual(server.listening, false); }); @@ -87,7 +101,11 @@ describe("abort signal", () => { t.plan(1); const server = createServer({}, handler); - server.listen({ signal: { aborted: true } }); + server.listen({ + signal: { + aborted: true, + }, + }); await once(server, "listening"); t.assert.strictEqual(server.listening, true); server.close(); diff --git a/test/close/double-close.test.js b/test/close/double-close.test.js index 76c2877..e2420de 100644 --- a/test/close/double-close.test.js +++ b/test/close/double-close.test.js @@ -2,8 +2,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("double close", () => { diff --git a/test/close/http2-session-timeout.test.js b/test/close/http2-session-timeout.test.js index f523626..7d7b078 100644 --- a/test/close/http2-session-timeout.test.js +++ b/test/close/http2-session-timeout.test.js @@ -24,11 +24,11 @@ describe("http2SessionTimeout", () => { test("http2s close with async-await", async () => { const server = createServer({ http2: true, + http2SessionTimeout: 100, https: { - key: global.context.privateKey, cert: global.context.certificate, + key: global.context.privateKey, }, - http2SessionTimeout: 100, }); await server.listen(); const session = connect(server.listeningOrigin, { @@ -60,7 +60,12 @@ describe("http2SessionTimeout", () => { const session = connect(server.listeningOrigin); session.on("error", () => {}); await once(session, "connect"); - const request = session.request({ ":method": "GET", ":path": "/" }).end(); + const request = session + .request({ + ":method": "GET", + ":path": "/", + }) + .end(); const [headers] = await once(request, "response"); t.assert.strictEqual(headers[":status"], 200); request.resume(); diff --git a/test/listen/dns.test.js b/test/listen/dns.test.js index d8ac4e7..12060a8 100644 --- a/test/listen/dns.test.js +++ b/test/listen/dns.test.js @@ -5,8 +5,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("dns", () => { @@ -102,9 +108,18 @@ describe("dns", () => { // we need to alter the first call only if (hostname === "localhost") { callback(null, [ - { address: "::1", family: 6 }, - { address: "127.0.0.1", family: 4 }, - { address: "0.0.0.0", family: 4 }, + { + address: "::1", + family: 6, + }, + { + address: "127.0.0.1", + family: 4, + }, + { + address: "0.0.0.0", + family: 4, + }, ]); } else { lookup(hostname, options, callback); diff --git a/test/listen/error.test.js b/test/listen/error.test.js index 346b0b3..db6b2d2 100644 --- a/test/listen/error.test.js +++ b/test/listen/error.test.js @@ -8,8 +8,14 @@ const { withResolvers } = require("../../lib/utils"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("error", () => { @@ -53,15 +59,25 @@ describe("error", () => { 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(); - }); - }); + 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; }); @@ -90,12 +106,16 @@ describe("error", () => { const server = createServer({}, handler); t.assert.strictEqual(server.listening, false); - await server.listen({ host: "127.0.0.1" }); + 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" }); + await server.listen({ + host: "127.0.0.1", + }); } catch (error) { t.assert.strictEqual(error.code, "ERR_SERVER_ALREADY_LISTEN"); } finally { @@ -117,10 +137,15 @@ describe("error", () => { const addresses = server.addresses(); t.assert.strictEqual(addresses.length, global.context.localhostCount); - conflict.listen({ port: address.port }, (error) => { - t.assert.strictEqual(error.code, "EADDRINUSE"); - server.close(resolve); - }); + conflict.listen( + { + port: address.port, + }, + (error) => { + t.assert.strictEqual(error.code, "EADDRINUSE"); + server.close(resolve); + }, + ); }); await promise; @@ -131,14 +156,17 @@ describe("error", () => { const { promise, resolve } = withResolvers(); const net = require("node:net"); const conflict = net.createServer(); - conflict.listen({ port: 0, host: "127.0.0.1" }); + conflict.listen({ + host: "127.0.0.1", + port: 0, + }); await once(conflict, "listening"); const server = createServer({}, handler); const listenOptions = { - port: conflict.address().port, host: "127.0.0.1", + port: conflict.address().port, }; server.listen(listenOptions, (error) => { t.assert.strictEqual(error.code, "EADDRINUSE"); diff --git a/test/listen/host.test.js b/test/listen/host.test.js index 42e7a46..ec6494f 100644 --- a/test/listen/host.test.js +++ b/test/listen/host.test.js @@ -5,8 +5,14 @@ const { withResolvers } = require("../../lib/utils"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("host/port", () => { @@ -99,7 +105,9 @@ describe("host/port", () => { resolve(); }); - server.listen({ host: "::1" }); + server.listen({ + host: "::1", + }); await promise; }); @@ -122,7 +130,9 @@ describe("host/port", () => { resolve(); }); - server.listen({ host: "127.0.0.1" }); + server.listen({ + host: "127.0.0.1", + }); await promise; }); @@ -145,7 +155,9 @@ describe("host/port", () => { resolve(); }); - server.listen({ host: null }); + server.listen({ + host: null, + }); await promise; }); @@ -168,7 +180,9 @@ describe("host/port", () => { resolve(); }); - server.listen({ host: undefined }); + server.listen({ + host: undefined, + }); await promise; }); @@ -187,13 +201,13 @@ describe("host/port", () => { server.listen( { - port: 0, - host: "localhost", backlog: 511, exclusive: false, + host: "localhost", + ipv6Only: false, + port: 0, readableAll: false, writableAll: false, - ipv6Only: false, }, (error) => { t.assert.ifError(error); diff --git a/test/listen/listen-after-close.test.js b/test/listen/listen-after-close.test.js index ed6926e..ed0ec4b 100644 --- a/test/listen/listen-after-close.test.js +++ b/test/listen/listen-after-close.test.js @@ -5,8 +5,14 @@ const { withResolvers } = require("../../lib/utils"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("listen again after close", () => { @@ -46,22 +52,32 @@ describe("listen again after close", () => { 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.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.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); + 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 index 7d35c96..6492a49 100644 --- a/test/listen/normalize.test.js +++ b/test/listen/normalize.test.js @@ -9,139 +9,284 @@ describe("listen options normalize", () => { const cases = [ { + actual: { + handle, + }, + expect: { + handle, + }, name: "{ handle }", - actual: { handle }, - expect: { handle }, }, { + actual: { + handle, + path: "", + }, + expect: { + handle, + }, name: "{ handle, path }", - actual: { handle, path: "" }, - expect: { handle }, }, { + actual: { + handle, + host: "127.0.0.1", + }, + expect: { + handle, + }, name: "{ handle, host }", - actual: { handle, host: "127.0.0.1" }, - expect: { handle }, }, { + actual: { + handle, + host: "127.0.0.1", + port: 1, + }, + expect: { + handle, + }, name: "{ handle, host, port }", - actual: { handle, host: "127.0.0.1", port: 1 }, - expect: { handle }, }, { + actual: { + path: "", + }, + expect: { + path: "", + }, name: "{ path }", - actual: { path: "" }, - expect: { path: "" }, }, { + actual: { + host: "127.0.0.1", + path: "", + }, + expect: { + path: "", + }, name: "{ path, host }", - actual: { path: "", host: "127.0.0.1" }, - expect: { path: "" }, }, { + actual: { + path: "", + port: 1, + }, + expect: { + host: "localhost", + port: 1, + }, name: "{ path, port }", - actual: { path: "", port: 1 }, - expect: { host: "localhost", port: 1 }, }, { + actual: { + host: "127.0.0.1", + path: "", + port: 1, + }, + expect: { + host: "127.0.0.1", + port: 1, + }, name: "{ path, host, port }", - actual: { path: "", host: "127.0.0.1", port: 1 }, - expect: { host: "127.0.0.1", port: 1 }, }, { + actual: { + port: 1, + }, + expect: { + host: "localhost", + port: 1, + }, name: "{ port }", - actual: { port: 1 }, - expect: { host: "localhost", port: 1 }, }, { + actual: { + host: "127.0.0.1", + }, + expect: { + host: "127.0.0.1", + port: 0, + }, name: "{ host }", - actual: { host: "127.0.0.1" }, - expect: { host: "127.0.0.1", port: 0 }, }, { + actual: { + host: "127.0.0.1", + port: 1, + }, + expect: { + host: "127.0.0.1", + port: 1, + }, name: "{ host, port }", - actual: { host: "127.0.0.1", port: 1 }, - expect: { host: "127.0.0.1", port: 1 }, }, { + actual: { + backlog: 1, + }, + expect: { + backlog: 1, + host: "localhost", + port: 0, + }, name: "{ backlog: 1 }", - actual: { backlog: 1 }, - expect: { host: "localhost", port: 0, backlog: 1 }, }, { + actual: { + backlog: true, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ backlog: true }", - actual: { backlog: true }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + exclusive: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ exclusive: 1 }", - actual: { exclusive: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + exclusive: true, + }, + expect: { + exclusive: true, + host: "localhost", + port: 0, + }, name: "{ exclusive: true }", - actual: { exclusive: true }, - expect: { host: "localhost", port: 0, exclusive: true }, }, { + actual: { + ipv6Only: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ ipv6Only: 1 }", - actual: { ipv6Only: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + ipv6Only: true, + }, + expect: { + host: "localhost", + ipv6Only: true, + port: 0, + }, name: "{ ipv6Only: true }", - actual: { ipv6Only: true }, - expect: { host: "localhost", port: 0, ipv6Only: true }, }, { + actual: { + reusePort: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ reusePort: 1 }", - actual: { reusePort: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + reusePort: true, + }, + expect: { + host: "localhost", + port: 0, + reusePort: true, + }, name: "{ reusePort: true }", - actual: { reusePort: true }, - expect: { host: "localhost", port: 0, reusePort: true }, }, { + actual: { + readableAll: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ readableAll: 1 }", - actual: { readableAll: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + readableAll: true, + }, + expect: { + host: "localhost", + port: 0, + readableAll: true, + }, name: "{ readableAll: true }", - actual: { readableAll: true }, - expect: { host: "localhost", port: 0, readableAll: true }, }, { + actual: { + writableAll: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ writableAll: 1 }", - actual: { writableAll: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + writableAll: true, + }, + expect: { + host: "localhost", + port: 0, + writableAll: true, + }, name: "{ writableAll: true }", - actual: { writableAll: true }, - expect: { host: "localhost", port: 0, writableAll: true }, }, { + actual: { + signal: 1, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ signal: 1 }", - actual: { signal: 1 }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + signal: {}, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ signal: {} }", - actual: { signal: {} }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + signal: null, + }, + expect: { + host: "localhost", + port: 0, + }, name: "{ signal: null }", - actual: { signal: null }, - expect: { host: "localhost", port: 0 }, }, { + actual: { + signal: abort.signal, + }, + expect: { + host: "localhost", + port: 0, + signal: abort.signal, + }, name: "{ signal }", - actual: { signal: abort.signal }, - expect: { host: "localhost", port: 0, signal: abort.signal }, }, ]; diff --git a/test/listen/socket.test.js b/test/listen/socket.test.js index 707920c..0a42e3a 100644 --- a/test/listen/socket.test.js +++ b/test/listen/socket.test.js @@ -7,8 +7,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("socket", () => { @@ -25,7 +31,9 @@ describe("socket", () => { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } - test(".listen({ path })", { skip: !sockPath }, async (t) => { + test(".listen({ path })", { + skip: !sockPath, + }, async (t) => { t.plan(5); const { promise, resolve } = withResolvers(); @@ -36,7 +44,9 @@ describe("socket", () => { t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); t.assert.strictEqual(addresses.length, 1); - t.assert.deepStrictEqual(addresses, [sockPath]); + t.assert.deepStrictEqual(addresses, [ + sockPath, + ]); server.close(); }); server.once("fastify.close", () => { @@ -44,24 +54,33 @@ describe("socket", () => { resolve(); }); - server.listen({ path: sockPath }); + server.listen({ + path: sockPath, + }); await promise; }); // Refs: https://github.com/fastify/fastify/pull/6937 - test(".listen({ path, host })", { skip: !sockPath }, async (t) => { + 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" }); + await server.listen({ + host: "localhost", + path: sockPath, + }); t.assert.strictEqual(server.listening, true); const addresses = server.addresses(); t.assert.strictEqual(addresses.length, 1); - t.assert.deepStrictEqual(addresses, [sockPath]); + t.assert.deepStrictEqual(addresses, [ + sockPath, + ]); await server.close(); }); diff --git a/test/listening-origin.test.js b/test/listening-origin.test.js index 262282e..0be4d8a 100644 --- a/test/listening-origin.test.js +++ b/test/listening-origin.test.js @@ -7,8 +7,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("listeningOrigin", () => { @@ -20,7 +26,12 @@ describe("listeningOrigin", () => { "lookup", (hostname, options, callback) => { if (hostname === "localhost") { - callback(null, [{ address: "127.0.0.1", family: 4 }]); + callback(null, [ + { + address: "127.0.0.1", + family: 4, + }, + ]); } else { lookup(hostname, options, callback); } @@ -46,7 +57,12 @@ describe("listeningOrigin", () => { "lookup", (hostname, options, callback) => { if (hostname === "localhost") { - callback(null, [{ address: "::1", family: 6 }]); + callback(null, [ + { + address: "::1", + family: 6, + }, + ]); } else { lookup(hostname, options, callback); } @@ -77,11 +93,15 @@ describe("listeningOrigin", () => { sockPath = `\\\\.\\pipe\\${(`${Math.random().toString(16)}0000000`).slice(2, 10)}-server-sock`; } - test("path", { skip: !sockPath }, async (t) => { + test("path", { + skip: !sockPath, + }, async (t) => { t.plan(1); const server = createServer({}, handler); - await server.listen({ path: sockPath }); + await server.listen({ + path: sockPath, + }); t.assert.strictEqual(server.listeningOrigin, sockPath); await server.close(); }); @@ -94,14 +114,24 @@ describe("listeningOrigin", () => { "lookup", (hostname, options, callback) => { if (hostname === "localhost") { - callback(null, [{ address: "127.0.0.1", family: 4 }]); + callback(null, [ + { + address: "127.0.0.1", + family: 4, + }, + ]); } else { lookup(hostname, options, callback); } }, ); - const server = createServer({ https: {} }, handler); + const server = createServer( + { + https: {}, + }, + handler, + ); await server.listen(); const address = server.address(); t.assert.strictEqual( @@ -120,14 +150,24 @@ describe("listeningOrigin", () => { "lookup", (hostname, options, callback) => { if (hostname === "localhost") { - callback(null, [{ address: "::1", family: 6 }]); + callback(null, [ + { + address: "::1", + family: 6, + }, + ]); } else { lookup(hostname, options, callback); } }, ); - const server = createServer({ https: {} }, handler); + const server = createServer( + { + https: {}, + }, + handler, + ); await server.listen(); const address = server.address(); t.assert.strictEqual( diff --git a/test/options/connection-timeout.test.js b/test/options/connection-timeout.test.js index 356c393..12307cd 100644 --- a/test/options/connection-timeout.test.js +++ b/test/options/connection-timeout.test.js @@ -7,8 +7,14 @@ const { kInternalServers } = require("../../lib/symbols"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("connectionTimeout", () => { @@ -16,31 +22,43 @@ describe("connectionTimeout", () => { test("[]", (t) => { t.plan(1); - const server = createServer({ connectionTimeout: [] }); + const server = createServer({ + connectionTimeout: [], + }); t.assert.strictEqual(server.timeout, 0); }); test("1.3", (t) => { t.plan(1); - const server = createServer({ connectionTimeout: 1.3 }); + const server = createServer({ + connectionTimeout: 1.3, + }); t.assert.strictEqual(server.timeout, 0); }); test("http", (t) => { t.plan(1); - const server = createServer({ connectionTimeout: 1 }); + const server = createServer({ + connectionTimeout: 1, + }); t.assert.strictEqual(server.timeout, 1); }); test("https", (t) => { t.plan(1); - const server = createServer({ connectionTimeout: 2, https: {} }); + 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 }); + const server = createServer({ + connectionTimeout: 3, + http2: true, + }); t.assert.strictEqual(server.timeout, 3); }); @@ -65,13 +83,21 @@ describe("connectionTimeout", () => { return server; } - const server = createServer({ connectionTimeout: 4, serverFactory }); + const server = createServer({ + connectionTimeout: 4, + serverFactory, + }); t.assert.strictEqual(server.timeout, 5); }); test("update all servers", async (t) => { t.plan(1 + global.context.localhostCount); - const server = createServer({ connectionTimeout: 1 }, handler); + const server = createServer( + { + connectionTimeout: 1, + }, + handler, + ); t.assert.strictEqual(server.timeout, 1); server.listen(); await once(server, "fastify.listening"); diff --git a/test/options/force-close-connections.test.js b/test/options/force-close-connections.test.js new file mode 100644 index 0000000..5a01c47 --- /dev/null +++ b/test/options/force-close-connections.test.js @@ -0,0 +1,117 @@ +"use strict"; + +const { describe, test } = require("node:test"); +const http2 = require("node:http2"); +const http = require("node:http"); +const { once } = require("node:stream"); +const { createServer } = require("../../lib"); + +const handler = (_request, response) => { + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); +}; + +describe("forceCloseConnections", () => { + describe("http2", () => { + test("session should be closed", async (t) => { + t.plan(2); + + const server = createServer({ + forceCloseConnections: true, + http2: true, + http2SessionTimeout: 100, + }); + server.on("stream", (stream) => { + stream.respond({ + [http2.constants.HTTP2_HEADER_STATUS]: 200, + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: + "application/json; charset=utf-8", + }); + stream.end("{}"); + }); + await server.listen(); + + const client = http2.connect(server.listeningOrigin); + const request1 = client.request({ + [http2.constants.HTTP2_HEADER_PATH]: "/", + [http2.constants.HTTP2_HEADER_METHOD]: "GET", + }); + await once(request1, "response"); + + server.close(); + + const request2 = client.request({ + [http2.constants.HTTP2_HEADER_PATH]: "/", + [http2.constants.HTTP2_HEADER_METHOD]: "GET", + }); + const [error] = await once(request2, "error"); + t.assert.strictEqual(error.code, "ERR_HTTP2_STREAM_ERROR"); + request2.end(); + + t.assert.strictEqual(client.closed, true); + client.destroy(); + }); + }); + + describe("http", () => { + test("shutdown while keep-alive connections are active", async (t) => { + t.plan(3); + + const server = createServer( + { + forceCloseConnections: true, + }, + handler, + ); + + await server.listen(); + + const keepAliveAgent = new http.Agent({ + keepAlive: true, + }); + const address = server.address(); + const socketName = keepAliveAgent.getName({ + host: address.address, + port: address.port, + }); + + const { promise, resolve, reject } = Promise.withResolvers(); + + http + .request( + server.listeningOrigin, + { + agent: keepAliveAgent, + }, + (response) => { + const sockets = keepAliveAgent.sockets[socketName]; + t.assert.strictEqual(response.headers.connection, "keep-alive"); + t.assert.strictEqual(sockets[0].closed, false); + + server.close((err) => { + if (err) { + reject(err); + } else { + // Due to the nature of the way we reap these keep-alive connections, + // there hasn't been enough time before the server fully closed in order + // for the client to have seen the socket get destroyed. The mere fact + // that we have reached this callback is enough indication that the + // feature being tested works as designed. + t.assert.strictEqual(sockets[0].closed, false); + resolve(); + } + }); + }, + ) + .end(); + + await promise; + }); + }); +}); diff --git a/test/options/keep-alive-timeout.test.js b/test/options/keep-alive-timeout.test.js index 1554793..0079879 100644 --- a/test/options/keep-alive-timeout.test.js +++ b/test/options/keep-alive-timeout.test.js @@ -7,8 +7,14 @@ const { kInternalServers } = require("../../lib/symbols"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("keepAliveTimeout", () => { @@ -16,40 +22,52 @@ describe("keepAliveTimeout", () => { test("[]", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: [] }); + const server = createServer({ + keepAliveTimeout: [], + }); t.assert.strictEqual(server.keepAliveTimeout, 72000); }); test("1.3", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: 1.3 }); + const server = createServer({ + keepAliveTimeout: 1.3, + }); t.assert.strictEqual(server.keepAliveTimeout, 72000); }); test("http", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: 1 }); + const server = createServer({ + keepAliveTimeout: 1, + }); t.assert.strictEqual(server.keepAliveTimeout, 1); }); test("https", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: 2, https: {} }); + const server = createServer({ + https: {}, + keepAliveTimeout: 2, + }); t.assert.strictEqual(server.keepAliveTimeout, 2); }); test("http2", (t) => { t.plan(1); - const server = createServer({ keepAliveTimeout: 3, http2: true }); + const server = createServer({ + http2: true, + keepAliveTimeout: 3, + }); t.assert.notStrictEqual(server.keepAliveTimeout, 3); }); test("http2 + https", (t) => { t.plan(1); const server = createServer({ - keepAliveTimeout: 3, http2: true, https: {}, + keepAliveTimeout: 3, }); t.assert.notStrictEqual(server.keepAliveTimeout, 3); }); @@ -65,13 +83,21 @@ describe("keepAliveTimeout", () => { return server; } - const server = createServer({ keepAliveTimeout: 4, serverFactory }); + const server = createServer({ + keepAliveTimeout: 4, + serverFactory, + }); t.assert.strictEqual(server.keepAliveTimeout, 5); }); test("update all servers", async (t) => { t.plan(1 + global.context.localhostCount); - const server = createServer({ keepAliveTimeout: 1 }, handler); + const server = createServer( + { + keepAliveTimeout: 1, + }, + handler, + ); t.assert.strictEqual(server.keepAliveTimeout, 1); server.listen(); await once(server, "fastify.listening"); diff --git a/test/options/max-requests-per-socket.test.js b/test/options/max-requests-per-socket.test.js index 77c3229..f549514 100644 --- a/test/options/max-requests-per-socket.test.js +++ b/test/options/max-requests-per-socket.test.js @@ -7,8 +7,14 @@ const { kInternalServers } = require("../../lib/symbols"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("maxRequestsPerSocket", () => { @@ -16,40 +22,53 @@ describe("maxRequestsPerSocket", () => { test("[]", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: [] }); + const server = createServer({ + maxRequestsPerSocket: [], + }); t.assert.strictEqual(server.maxRequestsPerSocket, 0); }); test("1.3", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 1.3 }); + const server = createServer({ + maxRequestsPerSocket: 1.3, + }); t.assert.strictEqual(server.maxRequestsPerSocket, 0); }); test("http", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 1, http: {} }); + const server = createServer({ + http: {}, + maxRequestsPerSocket: 1, + }); t.assert.strictEqual(server.maxRequestsPerSocket, 1); }); test("https", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 2, https: {} }); + const server = createServer({ + https: {}, + maxRequestsPerSocket: 2, + }); t.assert.strictEqual(server.maxRequestsPerSocket, 2); }); test("http2", (t) => { t.plan(1); - const server = createServer({ maxRequestsPerSocket: 3, http2: true }); + const server = createServer({ + http2: true, + maxRequestsPerSocket: 3, + }); t.assert.notStrictEqual(server.maxRequestsPerSocket, 3); }); test("http2 + https", (t) => { t.plan(1); const server = createServer({ - maxRequestsPerSocket: 3, http2: true, https: {}, + maxRequestsPerSocket: 3, }); t.assert.notStrictEqual(server.maxRequestsPerSocket, 3); }); @@ -65,13 +84,21 @@ describe("maxRequestsPerSocket", () => { return server; } - const server = createServer({ maxRequestsPerSocket: 4, serverFactory }); + const server = createServer({ + maxRequestsPerSocket: 4, + serverFactory, + }); t.assert.strictEqual(server.maxRequestsPerSocket, 5); }); test("update all servers", async (t) => { t.plan(1 + global.context.localhostCount); - const server = createServer({ maxRequestsPerSocket: 1 }, handler); + const server = createServer( + { + maxRequestsPerSocket: 1, + }, + handler, + ); t.assert.strictEqual(server.maxRequestsPerSocket, 1); server.listen(); await once(server, "fastify.listening"); diff --git a/test/options/request-timeout.test.js b/test/options/request-timeout.test.js index 844d441..e02a1eb 100644 --- a/test/options/request-timeout.test.js +++ b/test/options/request-timeout.test.js @@ -7,8 +7,14 @@ const { kInternalServers } = require("../../lib/symbols"); const { localhostCount } = require("../utils"); const handler = (_request, response) => { - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ data: "Hello World!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("requestTimeout", () => { @@ -16,37 +22,54 @@ describe("requestTimeout", () => { test("[]", (t) => { t.plan(1); - const server = createServer({ requestTimeout: [] }); + const server = createServer({ + requestTimeout: [], + }); t.assert.strictEqual(server.requestTimeout, 0); }); test("1.3", (t) => { t.plan(1); - const server = createServer({ requestTimeout: 1.3 }); + 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 }); + const server = createServer({ + http: true, + requestTimeout: 1, + }); t.assert.strictEqual(server.requestTimeout, 1); }); test("https", (t) => { t.plan(1); - const server = createServer({ requestTimeout: 2, https: true }); + const server = createServer({ + https: true, + requestTimeout: 2, + }); t.assert.strictEqual(server.requestTimeout, 2); }); test("http2", (t) => { t.plan(1); - const server = createServer({ requestTimeout: 3, http2: true }); + const server = createServer({ + http2: true, + requestTimeout: 3, + }); t.assert.notStrictEqual(server.requestTimeout, 3); }); test("http2 + https", (t) => { t.plan(1); - const server = createServer({ requestTimeout: 3, http2: true, https: {} }); + const server = createServer({ + http2: true, + https: {}, + requestTimeout: 3, + }); t.assert.notStrictEqual(server.requestTimeout, 3); }); @@ -61,13 +84,21 @@ describe("requestTimeout", () => { return server; } - const server = createServer({ requestTimeout: 4, serverFactory }); + const server = createServer({ + requestTimeout: 4, + serverFactory, + }); t.assert.strictEqual(server.requestTimeout, 5); }); test("update all servers", async (t) => { t.plan(1 + global.context.localhostCount); - const server = createServer({ requestTimeout: 1 }, handler); + const server = createServer( + { + requestTimeout: 1, + }, + handler, + ); t.assert.strictEqual(server.requestTimeout, 1); server.listen(); await once(server, "fastify.listening"); diff --git a/test/security.test.js b/test/security.test.js index 75bb722..2b4a233 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -4,8 +4,14 @@ 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!" })); + response.writeHead(200, { + "Content-Type": "application/json", + }); + response.end( + JSON.stringify({ + data: "Hello World!", + }), + ); }; describe("security", () => { diff --git a/test/utils.js b/test/utils.js index 882ed3b..bf478d3 100644 --- a/test/utils.js +++ b/test/utils.js @@ -13,12 +13,30 @@ function buildCertificate() { 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" }, + { + 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); @@ -38,15 +56,21 @@ function buildCertificate() { // correctly. function localhostCount(_, done) { if (!global.context?.localhostCount) { - lookup("localhost", { all: true }, (error, addresses) => { - if (error) { - done(error); - } else { - global.context ??= {}; - global.context.localhostCount = addresses.length; - done(); - } - }); + lookup( + "localhost", + { + all: true, + }, + (error, addresses) => { + if (error) { + done(error); + } else { + global.context ??= {}; + global.context.localhostCount = addresses.length; + done(); + } + }, + ); } } diff --git a/tsconfig.json b/tsconfig.json index 58484d3..25b1f8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,12 @@ { - "include": ["./types"], "compilerOptions": { - "types": ["node"], "allowJs": true, - "noEmit": true - } + "noEmit": true, + "types": [ + "node" + ] + }, + "include": [ + "./types" + ] } diff --git a/types/errors.d.ts b/types/errors.d.ts index 9a833f3..c4c897f 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -1,6 +1,8 @@ import type { FastifyErrorConstructor } from "@fastify/error"; export const ERR_SERVER_ALREADY_LISTEN: FastifyErrorConstructor< - { code: "ERR_SERVER_ALREADY_LISTEN" }, + { + code: "ERR_SERVER_ALREADY_LISTEN"; + }, [] >; diff --git a/types/index.tst.ts b/types/index.tst.ts index b26b4aa..9c8952e 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -9,11 +9,38 @@ import { } 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: {} }, () => {}), + 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({}, () => {}); diff --git a/types/server.d.ts b/types/server.d.ts index 93323cd..9d3f64b 100644 --- a/types/server.d.ts +++ b/types/server.d.ts @@ -23,6 +23,8 @@ interface CommonServerOptions { maxRequestsPerSocket?: number; /** @default 0 */ requestTimeout?: number; + /** @default false */ + forceCloseConnections?: boolean; } export interface HTTPServerOptions extends CommonServerOptions { From dfae69dce9fa9698410e5bbbff1142a65455e6a3 Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 19 Aug 2026 10:39:28 +0800 Subject: [PATCH 12/13] chore: remove .vscode --- .gitignore | 5 ++++- .vscode/settings.json | 7 ------- 2 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 7cf7be9..d409dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,7 @@ dist # Vite files vite.config.js.timestamp-* vite.config.ts.timestamp-* -.vite/ \ No newline at end of file +.vite/ + +# IDE Settings +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ff87b41..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "editor.defaultFormatter": "biomejs.biome", - "editor.codeActionsOnSave": { - "source.action.useSortedPackageJson.biome": "explicit", - "source.fixAll.biome": "explicit" - } -} From 61c3807e6aefc2434a4db2fdb98b6fc10f4aeb52 Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 19 Aug 2026 18:09:03 +0800 Subject: [PATCH 13/13] fixup --- types/errors.d.ts | 2 +- types/listen.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/types/errors.d.ts b/types/errors.d.ts index c4c897f..11aebb3 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -1,6 +1,6 @@ import type { FastifyErrorConstructor } from "@fastify/error"; -export const ERR_SERVER_ALREADY_LISTEN: FastifyErrorConstructor< +export type ERR_SERVER_ALREADY_LISTEN = FastifyErrorConstructor< { code: "ERR_SERVER_ALREADY_LISTEN"; }, diff --git a/types/listen.d.ts b/types/listen.d.ts index 25b05b5..0abed42 100644 --- a/types/listen.d.ts +++ b/types/listen.d.ts @@ -1,6 +1,6 @@ import type { AddressInfo, ListenOptions as NetListenOptions } from "node:net"; -import { ProxyServer, ServerState } from "."; -import { RequestListener, ServerOptions } from "./server"; +import type { ProxyServer, ServerState } from "."; +import type { RequestListener, ServerOptions } from "./server"; export interface ListenOptions extends NetListenOptions {}