--- /dev/null
+---
+'@sveltejs/adapter-node': minor
+---
+
+feat: add env vars for keepAliveTimeout and headersTimeout
When using systemd socket activation, `IDLE_TIMEOUT` specifies the number of seconds after which the app is automatically put to sleep when receiving no requests. If not set, the app runs continuously. See [Socket activation](#Socket-activation) for more details.
+### `KEEP_ALIVE_TIMEOUT` and `HEADERS_TIMEOUT`
+
+The number of seconds for [`keepAliveTimeout`](https://nodejs.org/api/http.html#serverkeepalivetimeout) and [`headersTimeout`](https://nodejs.org/api/http.html#serverheaderstimeout).
+
## Options
The adapter can be configured with various options:
declare module 'ENV' {
export function env(key: string, fallback?: any): string;
+ export function timeout_env(key: string, fallback?: any): number | undefined;
}
declare module 'HANDLER' {
'PORT_HEADER',
'BODY_SIZE_LIMIT',
'SHUTDOWN_TIMEOUT',
- 'IDLE_TIMEOUT'
+ 'IDLE_TIMEOUT',
+ 'KEEP_ALIVE_TIMEOUT',
+ 'HEADERS_TIMEOUT'
]);
const expected_unprefixed = new Set(['LISTEN_PID', 'LISTEN_FDS']);
const prefixed = prefix + name;
return prefixed in process.env ? process.env[prefixed] : fallback;
}
+
+const integer_regexp = /^\d+$/;
+
+/**
+ * Throw a consistently-structured parsing error for environment variables.
+ * @param {string} name
+ * @param {any} value
+ * @param {string} description
+ * @returns {never}
+ */
+function parsing_error(name, value, description) {
+ throw new Error(
+ `Invalid value for environment variable ${name}: ${JSON.stringify(value)} (${description})`
+ );
+}
+
+/**
+ * Check the environment for a timeout value (non-negative integer) in seconds.
+ * @param {string} name
+ * @param {number} [fallback]
+ * @returns {number | undefined}
+ */
+export function timeout_env(name, fallback) {
+ const raw = env(name, fallback);
+ if (!raw) {
+ return fallback;
+ }
+
+ if (!integer_regexp.test(raw)) {
+ parsing_error(name, raw, 'should be a non-negative integer');
+ }
+
+ const parsed = Number.parseInt(raw, 10);
+
+ // We don't technically need to check `Number.isNaN` because the value already passed the regexp test.
+ // However, just in case there's some new codepath introduced somewhere down the line, it's probably good
+ // to stick this in here.
+ if (Number.isNaN(parsed)) {
+ parsing_error(name, raw, 'should be a non-negative integer');
+ }
+
+ if (parsed < 0) {
+ parsing_error(name, raw, 'should be a non-negative integer');
+ }
+
+ return parsed;
+}
+import http from 'node:http';
import process from 'node:process';
import { handler } from 'HANDLER';
-import { env } from 'ENV';
+import { env, timeout_env } from 'ENV';
import polka from 'polka';
export const path = env('SOCKET_PATH', false);
/** @type {NodeJS.Timeout | void} */
let idle_timeout_id;
-const server = polka().use(handler);
+// Initialize the HTTP server here so that we can set properties before starting to listen.
+// Otherwise, polka delays creating the server until listen() is called. Settings these
+// properties after the server has started listening could lead to race conditions.
+const httpServer = http.createServer();
+
+const keep_alive_timeout = timeout_env('KEEP_ALIVE_TIMEOUT');
+if (keep_alive_timeout !== undefined) {
+ // Convert the keep-alive timeout from seconds to milliseconds (the unit Node.js expects).
+ httpServer.keepAliveTimeout = keep_alive_timeout * 1000;
+}
+
+const headers_timeout = timeout_env('HEADERS_TIMEOUT');
+if (headers_timeout !== undefined) {
+ // Convert the headers timeout from seconds to milliseconds (the unit Node.js expects).
+ httpServer.headersTimeout = headers_timeout * 1000;
+}
+
+const server = polka({ server: httpServer }).use(handler);
if (socket_activation) {
server.listen({ fd: SD_LISTEN_FDS_START }, () => {
// If a connection was opened with a keep-alive header close() will wait for the connection to
// time out rather than close it even if it is not handling any requests, so call this first
- // @ts-expect-error this was added in 18.2.0 but is not reflected in the types
- server.server.closeIdleConnections();
+ httpServer.closeIdleConnections();
- server.server.close((error) => {
+ httpServer.close((error) => {
// occurs if the server is already closed
if (error) return;
process.emit('sveltekit:shutdown', reason);
});
- shutdown_timeout_id = setTimeout(
- // @ts-expect-error this was added in 18.2.0 but is not reflected in the types
- () => server.server.closeAllConnections(),
- shutdown_timeout * 1000
- );
+ shutdown_timeout_id = setTimeout(() => httpServer.closeAllConnections(), shutdown_timeout * 1000);
}
-server.server.on(
+httpServer.on(
'request',
/** @param {import('node:http').IncomingMessage} req */
(req) => {
if (shutdown_timeout_id) {
// close connections as soon as they become idle, so they don't accept new requests
- // @ts-expect-error this was added in 18.2.0 but is not reflected in the types
- server.server.closeIdleConnections();
+ httpServer.closeIdleConnections();
}
if (requests === 0 && socket_activation && idle_timeout) {
idle_timeout_id = setTimeout(() => graceful_shutdown('IDLE'), idle_timeout * 1000);
--- /dev/null
+import { afterEach, expect, test, describe, vi } from 'vitest';
+import { timeout_env } from '../src/env.js';
+
+vi.hoisted(() => {
+ vi.stubGlobal('ENV_PREFIX', '');
+});
+
+describe('timeout_env', () => {
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ test('parses zero correctly', () => {
+ vi.stubEnv('TIMEOUT', '0');
+
+ const timeout = timeout_env('TIMEOUT');
+ expect(timeout).toBe(0);
+ });
+
+ test('parses positive integers correctly', () => {
+ vi.stubEnv('TIMEOUT', '60');
+
+ const timeout = timeout_env('TIMEOUT');
+ expect(timeout).toBe(60);
+ });
+
+ test('returns the fallback when variable is not set', () => {
+ const timeout = timeout_env('TIMEOUT', 30);
+ expect(timeout).toBe(30);
+ });
+
+ test('returns undefined when variable is not set and no fallback is provided', () => {
+ const timeout = timeout_env('TIMEOUT');
+ expect(timeout).toBeUndefined();
+ });
+
+ test('throws an error for negative integers', () => {
+ vi.stubEnv('TIMEOUT', '-10');
+
+ expect(() => timeout_env('TIMEOUT')).toThrow(
+ 'Invalid value for environment variable TIMEOUT: "-10" (should be a non-negative integer)'
+ );
+ });
+
+ test('throws an error for non-integer values', () => {
+ vi.stubEnv('TIMEOUT', 'abc');
+
+ expect(() => timeout_env('TIMEOUT')).toThrow(
+ 'Invalid value for environment variable TIMEOUT: "abc" (should be a non-negative integer)'
+ );
+ });
+});
+++ /dev/null
-import { expect, test, describe } from 'vitest';
-import { parse_as_bytes } from '../utils.js';
-
-describe('parse_as_bytes', () => {
- test('parses correctly', () => {
- const testData = {
- 200: 200,
- '512K': 512 * 1024,
- '200M': 200 * 1024 * 1024,
- '1G': 1024 * 1024 * 1024,
- Infinity,
- asdf: NaN
- };
-
- Object.keys(testData).forEach((input) => {
- const expected = testData[/** @type {keyof typeof testData} */ (input)];
- const actual = parse_as_bytes(input);
- expect(actual, `Testing input '${input}'`).toBe(expected);
- });
- });
-});
--- /dev/null
+import { expect, test, describe } from 'vitest';
+import { parse_as_bytes } from '../utils.js';
+
+describe('parse_as_bytes', () => {
+ test.each([
+ ['200', 200],
+ ['512K', 512 * 1024],
+ ['200M', 200 * 1024 * 1024],
+ ['1G', 1024 * 1024 * 1024],
+ ['Infinity', Infinity],
+ ['asdf', NaN]
+ ] as const)('parses correctly', (input, expected) => {
+ const actual = parse_as_bytes(input);
+ expect(actual, `Testing input '${input}'`).toBe(expected);
+ });
+});
"@sveltejs/kit": ["../kit/types/index"]
}
},
- "include": ["index.js", "src/**/*.js", "tests/**/*.js", "internal.d.ts", "utils.js"],
+ "include": [
+ "index.js",
+ "src/**/*.js",
+ "tests/**/*.js",
+ "tests/**/*.ts",
+ "internal.d.ts",
+ "utils.js"
+ ],
"exclude": ["tests/smoke.spec_disabled.js"]
}