import { bunEnv, bunExe, isASAN, isCI, nodeExe } from "harness"; import { createTest } from "node-harness"; import fs from "node:fs"; import http2 from "node:http2"; import net from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; import tls from "node:tls"; import { Duplex } from "stream"; import http2utils from "./helpers"; import { nodeEchoServer, TLS_CERT, TLS_OPTIONS } from "./http2-helpers"; const { describe, expect, it, createCallCheckCtx } = createTest(import.meta.path); const ASAN_MULTIPLIER = isASAN ? 3 : 1; function invalidArgTypeHelper(input) { if (input === null) return " Received null"; if (typeof input == "symbol") return ` Received type symbol`; if (typeof input == "object") return ` Received an instance of ${Object.prototype.toString.call(input).split(" ")[1]?.replace("]", "")?.replace("[", "")}`; if (typeof input == "string") return ` Received type string ('${input}')`; return ` Received type ${typeof input} (${input})`; } function paddingStrategyName(paddingStrategy) { switch (paddingStrategy) { case http2.constants.PADDING_STRATEGY_NONE: return "none"; case http2.constants.PADDING_STRATEGY_MAX: return "max"; case http2.constants.PADDING_STRATEGY_ALIGNED: return "aligned"; } } for (const nodeExecutable of [nodeExe(), bunExe()]) { for (const paddingStrategy of [ http2.constants.PADDING_STRATEGY_NONE, http2.constants.PADDING_STRATEGY_MAX, http2.constants.PADDING_STRATEGY_ALIGNED, ]) { describe.concurrent(`${path.basename(nodeExecutable)} ${paddingStrategyName(paddingStrategy)}`, () => { async function nodeDynamicServer(test_name, code) { if (!nodeExecutable) throw new Error("node executable not found"); const tmp_dir = path.join(fs.realpathSync(tmpdir()), "http.nodeDynamicServer"); if (!fs.existsSync(tmp_dir)) { fs.mkdirSync(tmp_dir, { recursive: true }); } const file_name = path.join(tmp_dir, test_name); const contents = Buffer.from(`const http2 = require("http2"); const server = http2.createServer({ paddingStrategy: ${paddingStrategy} }); ${code} server.listen(0); server.on("listening", () => { process.stdout.write(JSON.stringify(server.address())); });`); fs.writeFileSync(file_name, contents); const subprocess = Bun.spawn([nodeExecutable, file_name, JSON.stringify(TLS_CERT)], { stdout: "pipe", stdin: "inherit", stderr: "inherit", env: bunEnv, }); subprocess.unref(); const reader = subprocess.stdout.getReader(); const data = await reader.read(); const decoder = new TextDecoder("utf-8"); const text = decoder.decode(data.value); const address = JSON.parse(text); const url = `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`; return { address, url, subprocess }; } function doHttp2Request(HTTPS_SERVER, url, headers, payload, options, request_options) { const { promise, resolve, reject: promiseReject } = Promise.withResolvers(); if (url.startsWith(HTTPS_SERVER)) { options = { ...(options || {}), rejectUnauthorized: true, ...TLS_OPTIONS }; } const client = options ? http2.connect(url, options) : http2.connect(url); client.on("error", promiseReject); function reject(err) { promiseReject(err); client.close(); } const req = request_options ? client.request(headers, request_options) : client.request(headers); let response_headers = null; req.on("response", (headers, flags) => { response_headers = headers; }); req.setEncoding("utf8"); let data = ""; req.on("data", chunk => { data += chunk; }); req.on("error", reject); req.on("end", () => { resolve({ data, headers: response_headers }); client.close(); }); if (payload) { req.write(payload); } req.end(); return promise; } function doMultiplexHttp2Request(url, requests) { const { promise, resolve, reject: promiseReject } = Promise.withResolvers(); const client = http2.connect(url, TLS_OPTIONS); client.on("error", promiseReject); function reject(err) { promiseReject(err); client.close(); } let completed = 0; const results = []; for (let i = 0; i < requests.length; i++) { const { headers, payload } = requests[i]; const req = client.request(headers, { paddingStrategy: paddingStrategy, }); let response_headers = null; req.on("response", (headers, flags) => { response_headers = headers; }); req.setEncoding("utf8"); let data = ""; req.on("data", chunk => { data += chunk; }); req.on("error", reject); req.on("end", () => { results.push({ data, headers: response_headers }); completed++; if (completed === requests.length) { resolve(results); client.close(); } }); if (payload) { req.write(payload); } req.end(); } return promise; } describe("Client Basics", () => { // we dont support server yet but we support client it("should be able to send a GET request", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const result = await doHttp2Request(HTTPS_SERVER, HTTPS_SERVER, { ":path": "/get", "test-header": "test-value", }); let parsed; expect(() => (parsed = JSON.parse(result.data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/get`); expect(parsed.headers["test-header"]).toBe("test-value"); }); it("should be able to send a POST request", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const payload = JSON.stringify({ "hello": "bun" }); const result = await doHttp2Request( HTTPS_SERVER, HTTPS_SERVER, { ":path": "/post", "test-header": "test-value", ":method": "POST" }, payload, ); let parsed; expect(() => (parsed = JSON.parse(result.data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/post`); expect(parsed.headers["test-header"]).toBe("test-value"); expect(parsed.json).toEqual({ "hello": "bun" }); expect(parsed.data).toEqual(payload); }); it("should be able to send data using end", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const payload = JSON.stringify({ "hello": "bun" }); const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); const req = client.request({ ":path": "/post", "test-header": "test-value", ":method": "POST" }); let response_headers = null; req.on("response", (headers, flags) => { response_headers = headers; }); req.setEncoding("utf8"); let data = ""; req.on("data", chunk => { data += chunk; }); req.on("end", () => { resolve({ data, headers: response_headers }); client.close(); }); req.end(payload); const result = await promise; let parsed; expect(() => (parsed = JSON.parse(result.data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/post`); expect(parsed.headers["test-header"]).toBe("test-value"); expect(parsed.json).toEqual({ "hello": "bun" }); expect(parsed.data).toEqual(payload); }); it("should be able to mutiplex GET requests", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const results = await doMultiplexHttp2Request(HTTPS_SERVER, [ { headers: { ":path": "/get" } }, { headers: { ":path": "/get" } }, { headers: { ":path": "/get" } }, { headers: { ":path": "/get" } }, { headers: { ":path": "/get" } }, ]); expect(results.length).toBe(5); for (let i = 0; i < results.length; i++) { let parsed; expect(() => (parsed = JSON.parse(results[i].data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/get`); } }); it("http2 should receive remoteSettings when receiving default settings frame", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const session = http2.connect(HTTPS_SERVER, TLS_OPTIONS); session.once("remoteSettings", resolve); session.once("close", () => { reject(new Error("Failed to receive remoteSettings")); }); try { const settings = await promise; expect(settings).toBeDefined(); expect(settings).toEqual({ headerTableSize: 4096, enablePush: false, maxConcurrentStreams: 4294967295, initialWindowSize: 65535, maxFrameSize: 16384, maxHeaderListSize: 65535, maxHeaderSize: 65535, enableConnectProtocol: false, }); } finally { session.close(); } }); it("should be able to mutiplex POST requests", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const results = await doMultiplexHttp2Request(HTTPS_SERVER, [ { headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 1 }) }, { headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 2 }) }, { headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 3 }) }, { headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 4 }) }, { headers: { ":path": "/post", ":method": "POST" }, payload: JSON.stringify({ "request": 5 }) }, ]); expect(results.length).toBe(5); for (let i = 0; i < results.length; i++) { let parsed; expect(() => (parsed = JSON.parse(results[i].data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/post`); expect([1, 2, 3, 4, 5]).toContain(parsed.json?.request); } }); it("constants", () => { expect(http2.constants).toEqual({ "NGHTTP2_ERR_FRAME_SIZE_ERROR": -522, "NGHTTP2_SESSION_SERVER": 0, "NGHTTP2_SESSION_CLIENT": 1, "NGHTTP2_STREAM_STATE_IDLE": 1, "NGHTTP2_STREAM_STATE_OPEN": 2, "NGHTTP2_STREAM_STATE_RESERVED_LOCAL": 3, "NGHTTP2_STREAM_STATE_RESERVED_REMOTE": 4, "NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL": 5, "NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE": 6, "NGHTTP2_STREAM_STATE_CLOSED": 7, "NGHTTP2_FLAG_NONE": 0, "NGHTTP2_FLAG_END_STREAM": 1, "NGHTTP2_FLAG_END_HEADERS": 4, "NGHTTP2_FLAG_ACK": 1, "NGHTTP2_FLAG_PADDED": 8, "NGHTTP2_FLAG_PRIORITY": 32, "DEFAULT_SETTINGS_HEADER_TABLE_SIZE": 4096, "DEFAULT_SETTINGS_ENABLE_PUSH": 1, "DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS": 4294967295, "DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE": 65535, "DEFAULT_SETTINGS_MAX_FRAME_SIZE": 16384, "DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE": 65535, "DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL": 0, "MAX_MAX_FRAME_SIZE": 16777215, "MIN_MAX_FRAME_SIZE": 16384, "MAX_INITIAL_WINDOW_SIZE": 2147483647, "NGHTTP2_SETTINGS_HEADER_TABLE_SIZE": 1, "NGHTTP2_SETTINGS_ENABLE_PUSH": 2, "NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS": 3, "NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE": 4, "NGHTTP2_SETTINGS_MAX_FRAME_SIZE": 5, "NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE": 6, "NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL": 8, "PADDING_STRATEGY_NONE": 0, "PADDING_STRATEGY_ALIGNED": 1, "PADDING_STRATEGY_MAX": 2, "PADDING_STRATEGY_CALLBACK": 1, "NGHTTP2_NO_ERROR": 0, "NGHTTP2_PROTOCOL_ERROR": 1, "NGHTTP2_INTERNAL_ERROR": 2, "NGHTTP2_FLOW_CONTROL_ERROR": 3, "NGHTTP2_SETTINGS_TIMEOUT": 4, "NGHTTP2_STREAM_CLOSED": 5, "NGHTTP2_FRAME_SIZE_ERROR": 6, "NGHTTP2_REFUSED_STREAM": 7, "NGHTTP2_CANCEL": 8, "NGHTTP2_COMPRESSION_ERROR": 9, "NGHTTP2_CONNECT_ERROR": 10, "NGHTTP2_ENHANCE_YOUR_CALM": 11, "NGHTTP2_INADEQUATE_SECURITY": 12, "NGHTTP2_HTTP_1_1_REQUIRED": 13, "NGHTTP2_DEFAULT_WEIGHT": 16, "HTTP2_HEADER_STATUS": ":status", "HTTP2_HEADER_METHOD": ":method", "HTTP2_HEADER_AUTHORITY": ":authority", "HTTP2_HEADER_SCHEME": ":scheme", "HTTP2_HEADER_PATH": ":path", "HTTP2_HEADER_PROTOCOL": ":protocol", "HTTP2_HEADER_ACCEPT_ENCODING": "accept-encoding", "HTTP2_HEADER_ACCEPT_LANGUAGE": "accept-language", "HTTP2_HEADER_ACCEPT_RANGES": "accept-ranges", "HTTP2_HEADER_ACCEPT": "accept", "HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS": "access-control-allow-credentials", "HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS": "access-control-allow-headers", "HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS": "access-control-allow-methods", "HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN": "access-control-allow-origin", "HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS": "access-control-expose-headers", "HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS": "access-control-request-headers", "HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD": "access-control-request-method", "HTTP2_HEADER_AGE": "age", "HTTP2_HEADER_AUTHORIZATION": "authorization", "HTTP2_HEADER_CACHE_CONTROL": "cache-control", "HTTP2_HEADER_CONNECTION": "connection", "HTTP2_HEADER_CONTENT_DISPOSITION": "content-disposition", "HTTP2_HEADER_CONTENT_ENCODING": "content-encoding", "HTTP2_HEADER_CONTENT_LENGTH": "content-length", "HTTP2_HEADER_CONTENT_TYPE": "content-type", "HTTP2_HEADER_COOKIE": "cookie", "HTTP2_HEADER_DATE": "date", "HTTP2_HEADER_ETAG": "etag", "HTTP2_HEADER_FORWARDED": "forwarded", "HTTP2_HEADER_HOST": "host", "HTTP2_HEADER_IF_MODIFIED_SINCE": "if-modified-since", "HTTP2_HEADER_IF_NONE_MATCH": "if-none-match", "HTTP2_HEADER_IF_RANGE": "if-range", "HTTP2_HEADER_LAST_MODIFIED": "last-modified", "HTTP2_HEADER_LINK": "link", "HTTP2_HEADER_LOCATION": "location", "HTTP2_HEADER_RANGE": "range", "HTTP2_HEADER_REFERER": "referer", "HTTP2_HEADER_SERVER": "server", "HTTP2_HEADER_SET_COOKIE": "set-cookie", "HTTP2_HEADER_STRICT_TRANSPORT_SECURITY": "strict-transport-security", "HTTP2_HEADER_TRANSFER_ENCODING": "transfer-encoding", "HTTP2_HEADER_TE": "te", "HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS": "upgrade-insecure-requests", "HTTP2_HEADER_UPGRADE": "upgrade", "HTTP2_HEADER_USER_AGENT": "user-agent", "HTTP2_HEADER_VARY": "vary", "HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS": "x-content-type-options", "HTTP2_HEADER_X_FRAME_OPTIONS": "x-frame-options", "HTTP2_HEADER_KEEP_ALIVE": "keep-alive", "HTTP2_HEADER_PROXY_CONNECTION": "proxy-connection", "HTTP2_HEADER_X_XSS_PROTECTION": "x-xss-protection", "HTTP2_HEADER_ALT_SVC": "alt-svc", "HTTP2_HEADER_CONTENT_SECURITY_POLICY": "content-security-policy", "HTTP2_HEADER_EARLY_DATA": "early-data", "HTTP2_HEADER_EXPECT_CT": "expect-ct", "HTTP2_HEADER_ORIGIN": "origin", "HTTP2_HEADER_PURPOSE": "purpose", "HTTP2_HEADER_TIMING_ALLOW_ORIGIN": "timing-allow-origin", "HTTP2_HEADER_X_FORWARDED_FOR": "x-forwarded-for", "HTTP2_HEADER_PRIORITY": "priority", "HTTP2_HEADER_ACCEPT_CHARSET": "accept-charset", "HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE": "access-control-max-age", "HTTP2_HEADER_ALLOW": "allow", "HTTP2_HEADER_CONTENT_LANGUAGE": "content-language", "HTTP2_HEADER_CONTENT_LOCATION": "content-location", "HTTP2_HEADER_CONTENT_MD5": "content-md5", "HTTP2_HEADER_CONTENT_RANGE": "content-range", "HTTP2_HEADER_DNT": "dnt", "HTTP2_HEADER_EXPECT": "expect", "HTTP2_HEADER_EXPIRES": "expires", "HTTP2_HEADER_FROM": "from", "HTTP2_HEADER_IF_MATCH": "if-match", "HTTP2_HEADER_IF_UNMODIFIED_SINCE": "if-unmodified-since", "HTTP2_HEADER_MAX_FORWARDS": "max-forwards", "HTTP2_HEADER_PREFER": "prefer", "HTTP2_HEADER_PROXY_AUTHENTICATE": "proxy-authenticate", "HTTP2_HEADER_PROXY_AUTHORIZATION": "proxy-authorization", "HTTP2_HEADER_REFRESH": "refresh", "HTTP2_HEADER_RETRY_AFTER": "retry-after", "HTTP2_HEADER_TRAILER": "trailer", "HTTP2_HEADER_TK": "tk", "HTTP2_HEADER_VIA": "via", "HTTP2_HEADER_WARNING": "warning", "HTTP2_HEADER_WWW_AUTHENTICATE": "www-authenticate", "HTTP2_HEADER_HTTP2_SETTINGS": "http2-settings", "HTTP2_METHOD_ACL": "ACL", "HTTP2_METHOD_BASELINE_CONTROL": "BASELINE-CONTROL", "HTTP2_METHOD_BIND": "BIND", "HTTP2_METHOD_CHECKIN": "CHECKIN", "HTTP2_METHOD_CHECKOUT": "CHECKOUT", "HTTP2_METHOD_CONNECT": "CONNECT", "HTTP2_METHOD_COPY": "COPY", "HTTP2_METHOD_DELETE": "DELETE", "HTTP2_METHOD_GET": "GET", "HTTP2_METHOD_HEAD": "HEAD", "HTTP2_METHOD_LABEL": "LABEL", "HTTP2_METHOD_LINK": "LINK", "HTTP2_METHOD_LOCK": "LOCK", "HTTP2_METHOD_MERGE": "MERGE", "HTTP2_METHOD_MKACTIVITY": "MKACTIVITY", "HTTP2_METHOD_MKCALENDAR": "MKCALENDAR", "HTTP2_METHOD_MKCOL": "MKCOL", "HTTP2_METHOD_MKREDIRECTREF": "MKREDIRECTREF", "HTTP2_METHOD_MKWORKSPACE": "MKWORKSPACE", "HTTP2_METHOD_MOVE": "MOVE", "HTTP2_METHOD_OPTIONS": "OPTIONS", "HTTP2_METHOD_ORDERPATCH": "ORDERPATCH", "HTTP2_METHOD_PATCH": "PATCH", "HTTP2_METHOD_POST": "POST", "HTTP2_METHOD_PRI": "PRI", "HTTP2_METHOD_PROPFIND": "PROPFIND", "HTTP2_METHOD_PROPPATCH": "PROPPATCH", "HTTP2_METHOD_PUT": "PUT", "HTTP2_METHOD_REBIND": "REBIND", "HTTP2_METHOD_REPORT": "REPORT", "HTTP2_METHOD_SEARCH": "SEARCH", "HTTP2_METHOD_TRACE": "TRACE", "HTTP2_METHOD_UNBIND": "UNBIND", "HTTP2_METHOD_UNCHECKOUT": "UNCHECKOUT", "HTTP2_METHOD_UNLINK": "UNLINK", "HTTP2_METHOD_UNLOCK": "UNLOCK", "HTTP2_METHOD_UPDATE": "UPDATE", "HTTP2_METHOD_UPDATEREDIRECTREF": "UPDATEREDIRECTREF", "HTTP2_METHOD_VERSION_CONTROL": "VERSION-CONTROL", "HTTP_STATUS_CONTINUE": 100, "HTTP_STATUS_SWITCHING_PROTOCOLS": 101, "HTTP_STATUS_PROCESSING": 102, "HTTP_STATUS_EARLY_HINTS": 103, "HTTP_STATUS_OK": 200, "HTTP_STATUS_CREATED": 201, "HTTP_STATUS_ACCEPTED": 202, "HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION": 203, "HTTP_STATUS_NO_CONTENT": 204, "HTTP_STATUS_RESET_CONTENT": 205, "HTTP_STATUS_PARTIAL_CONTENT": 206, "HTTP_STATUS_MULTI_STATUS": 207, "HTTP_STATUS_ALREADY_REPORTED": 208, "HTTP_STATUS_IM_USED": 226, "HTTP_STATUS_MULTIPLE_CHOICES": 300, "HTTP_STATUS_MOVED_PERMANENTLY": 301, "HTTP_STATUS_FOUND": 302, "HTTP_STATUS_SEE_OTHER": 303, "HTTP_STATUS_NOT_MODIFIED": 304, "HTTP_STATUS_USE_PROXY": 305, "HTTP_STATUS_TEMPORARY_REDIRECT": 307, "HTTP_STATUS_PERMANENT_REDIRECT": 308, "HTTP_STATUS_BAD_REQUEST": 400, "HTTP_STATUS_UNAUTHORIZED": 401, "HTTP_STATUS_PAYMENT_REQUIRED": 402, "HTTP_STATUS_FORBIDDEN": 403, "HTTP_STATUS_NOT_FOUND": 404, "HTTP_STATUS_METHOD_NOT_ALLOWED": 405, "HTTP_STATUS_NOT_ACCEPTABLE": 406, "HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED": 407, "HTTP_STATUS_REQUEST_TIMEOUT": 408, "HTTP_STATUS_CONFLICT": 409, "HTTP_STATUS_GONE": 410, "HTTP_STATUS_LENGTH_REQUIRED": 411, "HTTP_STATUS_PRECONDITION_FAILED": 412, "HTTP_STATUS_PAYLOAD_TOO_LARGE": 413, "HTTP_STATUS_URI_TOO_LONG": 414, "HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE": 415, "HTTP_STATUS_RANGE_NOT_SATISFIABLE": 416, "HTTP_STATUS_EXPECTATION_FAILED": 417, "HTTP_STATUS_TEAPOT": 418, "HTTP_STATUS_MISDIRECTED_REQUEST": 421, "HTTP_STATUS_UNPROCESSABLE_ENTITY": 422, "HTTP_STATUS_LOCKED": 423, "HTTP_STATUS_FAILED_DEPENDENCY": 424, "HTTP_STATUS_TOO_EARLY": 425, "HTTP_STATUS_UPGRADE_REQUIRED": 426, "HTTP_STATUS_PRECONDITION_REQUIRED": 428, "HTTP_STATUS_TOO_MANY_REQUESTS": 429, "HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE": 431, "HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS": 451, "HTTP_STATUS_INTERNAL_SERVER_ERROR": 500, "HTTP_STATUS_NOT_IMPLEMENTED": 501, "HTTP_STATUS_BAD_GATEWAY": 502, "HTTP_STATUS_SERVICE_UNAVAILABLE": 503, "HTTP_STATUS_GATEWAY_TIMEOUT": 504, "HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED": 505, "HTTP_STATUS_VARIANT_ALSO_NEGOTIATES": 506, "HTTP_STATUS_INSUFFICIENT_STORAGE": 507, "HTTP_STATUS_LOOP_DETECTED": 508, "HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED": 509, "HTTP_STATUS_NOT_EXTENDED": 510, "HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED": 511, }); }); it("getDefaultSettings", () => { const settings = http2.getDefaultSettings(); expect(settings).toEqual({ enableConnectProtocol: false, headerTableSize: 4096, enablePush: false, initialWindowSize: 65535, maxFrameSize: 16384, maxConcurrentStreams: 4294967295, maxHeaderListSize: 65535, maxHeaderSize: 65535, }); }); it("getPackedSettings/getUnpackedSettings", () => { const settings = { headerTableSize: 1, enablePush: false, initialWindowSize: 2, maxFrameSize: 32768, maxConcurrentStreams: 4, maxHeaderListSize: 5, maxHeaderSize: 5, enableConnectProtocol: false, }; const buffer = http2.getPackedSettings(settings); expect(buffer.byteLength).toBe(36); expect(http2.getUnpackedSettings(buffer)).toEqual(settings); }); it("getUnpackedSettings should throw if buffer is too small", () => { const buffer = new ArrayBuffer(1); expect(() => http2.getUnpackedSettings(buffer)).toThrow( /Expected buf to be a Buffer of at least 6 bytes and a multiple of 6 bytes/, ); }); it("getUnpackedSettings should throw if buffer is not a multiple of 6 bytes", () => { const buffer = new ArrayBuffer(7); expect(() => http2.getUnpackedSettings(buffer)).toThrow( /Expected buf to be a Buffer of at least 6 bytes and a multiple of 6 bytes/, ); }); it("getUnpackedSettings should throw if buffer is not a buffer", () => { const buffer = {}; expect(() => http2.getUnpackedSettings(buffer)).toThrow(/Expected buf to be a Buffer/); }); it("headers cannot be bigger than 65536 bytes", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; try { await doHttp2Request(HTTPS_SERVER, HTTPS_SERVER, { ":path": "/", "test-header": "A".repeat(90000) }); expect("unreachable").toBe(true); } catch (err) { expect(err.code).toBe("ERR_HTTP2_STREAM_ERROR"); expect(err.message).toBe("Stream closed with error code NGHTTP2_COMPRESSION_ERROR"); } }); it("should be destroyed after close", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject: promiseReject } = Promise.withResolvers(); const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS); client.on("error", promiseReject); client.on("close", resolve); function reject(err) { promiseReject(err); client.close(); } const req = client.request({ ":path": "/get", }); req.resume(); req.on("error", reject); req.on("end", () => { client.close(); }); req.end(); await promise; expect(client.destroyed).toBe(true); }); it("should be destroyed after destroy", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject: promiseReject } = Promise.withResolvers(); const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS); client.on("error", promiseReject); client.on("close", resolve); function reject(err) { promiseReject(err); client.destroy(); } const req = client.request({ ":path": "/get", }); req.on("error", reject); req.resume(); req.on("end", () => { client.destroy(); }); req.end(); await promise; expect(client.destroyed).toBe(true); }); it("should fail to connect over HTTP/1.1", async () => { await using server_ = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server_.url; const tlsCert = TLS_CERT; using server = Bun.serve({ port: 0, hostname: "127.0.0.1", tls: { ...tlsCert, ca: TLS_CERT.ca, }, fetch() { return new Response("hello"); }, }); const url = `https://127.0.0.1:${server.port}`; try { await doHttp2Request(HTTPS_SERVER, url, { ":path": "/" }, null, TLS_OPTIONS); expect("unreachable").toBe(true); } catch (err) { expect(err.code).toBe("ERR_HTTP2_ERROR"); } }); it("works with Duplex", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; class JSSocket extends Duplex { constructor(socket) { super({ emitClose: true }); socket.on("close", () => this.destroy()); socket.on("data", data => this.push(data)); this.socket = socket; } _write(data, encoding, callback) { this.socket.write(data, encoding, callback); } _read(size) {} _final(cb) { cb(); } } const { promise, resolve, reject } = Promise.withResolvers(); const socket = tls .connect( { rejectUnauthorized: false, host: new URL(HTTPS_SERVER).hostname, port: new URL(HTTPS_SERVER).port, ALPNProtocols: ["h2"], ...TLS_OPTIONS, }, () => { doHttp2Request(HTTPS_SERVER, `${HTTPS_SERVER}/get`, { ":path": "/get" }, null, { createConnection: () => { return new JSSocket(socket); }, }).then(resolve, reject); }, ) .on("error", reject); const result = await promise; let parsed; expect(() => (parsed = JSON.parse(result.data))).not.toThrow(); expect(parsed.url).toBe(`${HTTPS_SERVER}/get`); socket.destroy(); }); it("close callback", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(`${HTTPS_SERVER}/get`, TLS_OPTIONS); client.on("error", reject); client.close(resolve); await promise; expect(client.destroyed).toBe(true); }); it("is possible to abort request", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const abortController = new AbortController(); const promise = doHttp2Request(HTTPS_SERVER, `${HTTPS_SERVER}/get`, { ":path": "/get" }, null, null, { signal: abortController.signal, }); abortController.abort(); try { await promise; expect("unreachable").toBe(true); } catch (err) { expect(err.code).toBe("ABORT_ERR"); } }); it("aborted event should work with abortController", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const abortController = new AbortController(); const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); const req = client.request({ ":path": "/post", ":method": "POST" }, { signal: abortController.signal }); req.on("aborted", resolve); req.on("error", err => { if (err.code !== "ABORT_ERR") { reject(err); } }); req.on("end", () => { reject(); client.close(); }); abortController.abort(); const result = await promise; expect(result).toBeUndefined(); expect(req.aborted).toBeTrue(); expect(req.rstCode).toBe(http2.constants.NGHTTP2_CANCEL); }); it("aborted event should work with aborted signal", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); const req = client.request({ ":path": "/post", ":method": "POST" }, { signal: AbortSignal.abort() }); req.on("aborted", reject); // will not be emited because we could not start the request at all req.on("error", err => { if (err.name !== "AbortError") { reject(err); } else { resolve(); } }); req.on("end", () => { client.close(); }); const result = await promise; expect(result).toBeUndefined(); expect(req.rstCode).toBe(http2.constants.NGHTTP2_CANCEL); expect(req.aborted).toBeTrue(); // will be true in this case }); it("state should work", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); const req = client.request({ ":path": "/", "test-header": "test-value" }); { const state = req.state; expect(typeof state).toBe("object"); expect(typeof state.state).toBe("number"); expect(typeof state.weight).toBe("number"); expect(typeof state.sumDependencyWeight).toBe("number"); expect(typeof state.localClose).toBe("number"); expect(typeof state.remoteClose).toBe("number"); expect(typeof state.localWindowSize).toBe("number"); } // Test Session State. { const state = client.state; expect(typeof state).toBe("object"); expect(typeof state.effectiveLocalWindowSize).toBe("number"); expect(typeof state.effectiveRecvDataLength).toBe("number"); expect(typeof state.nextStreamID).toBe("number"); expect(typeof state.localWindowSize).toBe("number"); expect(typeof state.lastProcStreamID).toBe("number"); expect(typeof state.remoteWindowSize).toBe("number"); expect(typeof state.outboundQueueSize).toBe("number"); expect(typeof state.deflateDynamicTableSize).toBe("number"); expect(typeof state.inflateDynamicTableSize).toBe("number"); } let response_headers = null; req.on("response", (headers, flags) => { response_headers = headers; }); req.resume(); req.on("end", () => { resolve(); client.close(); }); await promise; expect(response_headers[":status"]).toBe(200); }); it("settings and properties should work", async () => { const assertSettings = settings => { expect(settings).toBeDefined(); expect(typeof settings).toBe("object"); expect(typeof settings.headerTableSize).toBe("number"); expect(typeof settings.enablePush).toBe("boolean"); expect(typeof settings.initialWindowSize).toBe("number"); expect(typeof settings.maxFrameSize).toBe("number"); expect(typeof settings.maxConcurrentStreams).toBe("number"); expect(typeof settings.maxHeaderListSize).toBe("number"); expect(typeof settings.maxHeaderSize).toBe("number"); }; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect("https://www.example.com"); client.on("error", reject); expect(client.connecting).toBeTrue(); expect(client.alpnProtocol).toBeUndefined(); expect(client.encrypted).toBeTrue(); expect(client.closed).toBeFalse(); expect(client.destroyed).toBeFalse(); expect(client.originSet.length).toBe(1); expect(client.pendingSettingsAck).toBeTrue(); assertSettings(client.localSettings); expect(client.remoteSettings).toBeNull(); const headers = { ":path": "/" }; const req = client.request(headers); expect(req.closed).toBeFalse(); expect(req.destroyed).toBeFalse(); // we always asign a stream id to the request expect(req.pending).toBeFalse(); expect(typeof req.id).toBe("number"); expect(req.session).toBeDefined(); expect(req.sentHeaders).toEqual({ ":authority": "www.example.com", ":method": "GET", ":path": "/", ":scheme": "https", }); expect(req.sentTrailers).toBeUndefined(); expect(req.sentInfoHeaders.length).toBe(0); expect(req.scheme).toBe("https"); let response_headers = null; req.on("response", (headers, flags) => { response_headers = headers; }); req.resume(); req.on("end", () => { resolve(); }); await promise; expect(response_headers[":status"]).toBe(200); const settings = client.remoteSettings; const localSettings = client.localSettings; assertSettings(settings); assertSettings(localSettings); expect(settings).toEqual(client.remoteSettings); expect(localSettings).toEqual(client.localSettings); client.destroy(); expect(client.connecting).toBeFalse(); expect(client.alpnProtocol).toBe("h2"); expect(client.pendingSettingsAck).toBeFalse(); expect(client.destroyed).toBeTrue(); expect(client.closed).toBeTrue(); expect(req.closed).toBeTrue(); expect(req.destroyed).toBeTrue(); expect(req.rstCode).toBe(http2.constants.NGHTTP2_NO_ERROR); }); it("ping events should work", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); client.on("connect", () => { client.ping(Buffer.from("12345678"), (err, duration, payload) => { if (err) { reject(err); } else { resolve({ duration, payload }); } client.close(); }); }); let received_ping; client.on("ping", payload => { received_ping = payload; }); const result = await promise; expect(typeof result.duration).toBe("number"); expect(result.payload).toBeInstanceOf(Buffer); expect(result.payload.byteLength).toBe(8); expect(received_ping).toBeInstanceOf(Buffer); expect(received_ping.byteLength).toBe(8); expect(received_ping).toEqual(result.payload); expect(received_ping).toEqual(Buffer.from("12345678")); }); it("ping without events should work", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); client.on("connect", () => { client.ping((err, duration, payload) => { if (err) { reject(err); } else { resolve({ duration, payload }); } client.close(); }); }); let received_ping; client.on("ping", payload => { received_ping = payload; }); const result = await promise; expect(typeof result.duration).toBe("number"); expect(result.payload).toBeInstanceOf(Buffer); expect(result.payload.byteLength).toBe(8); expect(received_ping).toBeInstanceOf(Buffer); expect(received_ping.byteLength).toBe(8); expect(received_ping).toEqual(result.payload); }); it("ping with wrong payload length events should error", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); client.on("connect", () => { client.ping(Buffer.from("oops"), (err, duration, payload) => { if (err) { resolve(err); } else { reject("unreachable"); } client.close(); }); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_PING_LENGTH"); }); it("ping with wrong payload type events should throw", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); client.on("connect", () => { try { client.ping("oops", (err, duration, payload) => { reject("unreachable"); client.close(); }); } catch (err) { resolve(err); client.close(); } }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_INVALID_ARG_TYPE"); }); it("stream event should work", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); client.on("stream", stream => { resolve(stream); client.close(); }); client.request({ ":path": "/" }).end(); const stream = await promise; expect(stream).toBeDefined(); expect(stream.id).toBe(1); }); it("wantTrailers should work", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); client.on("error", reject); const headers = { ":path": "/", ":method": "POST", "x-wait-trailer": "true" }; const req = client.request(headers, { waitForTrailers: true, }); req.setEncoding("utf8"); let response_headers; req.on("response", headers => { response_headers = headers; }); let trailers = { "x-trailer": "hello" }; req.on("wantTrailers", () => { req.sendTrailers(trailers); }); let data = ""; req.on("data", chunk => { data += chunk; client.close(); }); req.on("error", reject); req.on("end", () => { resolve({ data, headers: response_headers }); client.close(); }); req.end("hello"); const response = await promise; let parsed; expect(() => (parsed = JSON.parse(response.data))).not.toThrow(); expect(parsed.headers[":method"]).toEqual(headers[":method"]); expect(parsed.headers[":path"]).toEqual(headers[":path"]); expect(parsed.headers["x-wait-trailer"]).toEqual(headers["x-wait-trailer"]); expect(parsed.trailers).toEqual(trailers); expect(response.headers[":status"]).toBe(200); expect(response.headers["set-cookie"]).toEqual([ "a=b", "c=d; Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly", "e=f", ]); }); it.skipIf(!isCI)( "should not leak memory", async () => { await using server = await nodeEchoServer(paddingStrategy); await using proc = Bun.spawn({ cmd: [bunExe(), "--smol", "run", path.join(import.meta.dir, "node-http2-memory-leak.js")], env: { ...bunEnv, BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString("10"), HTTP2_SERVER_INFO: JSON.stringify(server), HTTP2_SERVER_TLS: JSON.stringify(TLS_OPTIONS), }, stderr: "inherit", stdin: "inherit", stdout: "inherit", }); const exitCode = await proc.exited; expect(exitCode || 0).toBe(0); }, 100000, ); it("should receive goaway", async () => { const { promise, resolve, reject } = Promise.withResolvers(); const server = await nodeDynamicServer( "http2.away.1.js", ` server.on("stream", (stream, headers, flags) => { stream.session.goaway(http2.constants.NGHTTP2_CONNECT_ERROR, 0, Buffer.from("123456")); }); `, ); try { const client = http2.connect(server.url); client.on("goaway", (...params) => resolve(params)); client.on("error", reject); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.on("error", err => { if (err.errno !== http2.constants.NGHTTP2_CONNECT_ERROR) { reject(err); } }); req.end(); }); const result = await promise; expect(result).toBeDefined(); const [code, lastStreamID, opaqueData] = result; expect(code).toBe(http2.constants.NGHTTP2_CONNECT_ERROR); expect(lastStreamID).toBe(1); expect(opaqueData.toString()).toBe("123456"); } finally { server.subprocess.kill(); } }); it("should receive goaway without debug data", async () => { const { promise, resolve, reject } = Promise.withResolvers(); const server = await nodeDynamicServer( "http2.away.2.js", ` server.on("stream", (stream, headers, flags) => { stream.session.goaway(http2.constants.NGHTTP2_CONNECT_ERROR, 0); }); `, ); try { const client = http2.connect(server.url); client.on("goaway", (...params) => resolve(params)); client.on("error", reject); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.on("error", err => { if (err.errno !== http2.constants.NGHTTP2_CONNECT_ERROR) { reject(err); } }); req.end(); }); const result = await promise; expect(result).toBeDefined(); const [code, lastStreamID, opaqueData] = result; expect(code).toBe(http2.constants.NGHTTP2_CONNECT_ERROR); expect(lastStreamID).toBe(1); expect(opaqueData.toString()).toBe(""); } finally { server.subprocess.kill(); } }); it("should not be able to write on socket", async () => { await using server = await nodeEchoServer(paddingStrategy); const HTTPS_SERVER = server.url; const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS, (session, socket) => { try { client.socket.write("hello"); client.socket.end(); reject(new Error("unreachable")); } catch (err) { try { expect(err.code).toBe("ERR_HTTP2_NO_SOCKET_MANIPULATION"); resolve(); } catch (err2) { reject(err2); } } }); await promise; }); it("should handle bad GOAWAY server frame size", async () => { const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); const frame = new http2utils.Frame(7, 7, 0, 0).data; socket.write(Buffer.concat([frame, Buffer.alloc(7)])); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_FRAME_SIZE_ERROR"); } finally { server.close(); } }); it("should handle bad DATA_FRAME server frame size", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const frame = new http2utils.DataFrame(1, Buffer.alloc(16384 * 2), 0, 1).data; socket.write(frame); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_FRAME_SIZE_ERROR"); } finally { server.close(); } }); it("should handle bad RST_FRAME server frame size (no stream)", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const frame = new http2utils.Frame(4, 3, 0, 0).data; socket.write(Buffer.concat([frame, Buffer.alloc(4)])); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_PROTOCOL_ERROR"); } finally { server.close(); } }); it("should handle bad RST_FRAME server frame size (less than allowed)", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const frame = new http2utils.Frame(3, 3, 0, 1).data; socket.write(Buffer.concat([frame, Buffer.alloc(3)])); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_FRAME_SIZE_ERROR"); } finally { server.close(); } }); it("should handle bad RST_FRAME server frame size (more than allowed)", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const buffer = Buffer.alloc(16384 * 2); const frame = new http2utils.Frame(buffer.byteLength, 3, 0, 1).data; socket.write(Buffer.concat([frame, buffer])); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_FRAME_SIZE_ERROR"); } finally { server.close(); } }); it("should handle bad CONTINUATION_FRAME server frame size", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const frame = new http2utils.HeadersFrame(1, http2utils.kFakeResponseHeaders, 0, true, false); socket.write(frame.data); const continuationFrame = new http2utils.ContinuationFrame( 1, http2utils.kFakeResponseHeaders, 0, true, false, ); socket.write(continuationFrame.data); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_PROTOCOL_ERROR"); } finally { server.close(); } }); it("should handle bad PRIOTITY_FRAME server frame size", async () => { const { promise: waitToWrite, resolve: allowWrite } = Promise.withResolvers(); const { promise: serverListening, resolve: serverResolve } = Promise.withResolvers(); const server = net.createServer(async socket => { const settings = new http2utils.SettingsFrame(true); socket.write(settings.data); await waitToWrite; const frame = new http2utils.Frame(4, 2, 0, 1).data; socket.write(Buffer.concat([frame, Buffer.alloc(4)])); }); server.listen(0, "127.0.0.1", () => serverResolve()); await serverListening; const url = `http://127.0.0.1:${server.address().port}`; try { const { promise, resolve } = Promise.withResolvers(); const client = http2.connect(url); client.on("error", resolve); client.on("connect", () => { const req = client.request({ ":path": "/" }); req.end(); allowWrite(); }); const result = await promise; expect(result).toBeDefined(); expect(result.code).toBe("ERR_HTTP2_SESSION_ERROR"); expect(result.message).toBe("Session closed with error code NGHTTP2_FRAME_SIZE_ERROR"); } finally { server.close(); } }); }); }); } } it("sensitive headers should work", async () => { const server = http2.createServer(); let client; try { const { promise, resolve, reject } = Promise.withResolvers(); server.on("stream", stream => { stream.respond({ ":status": 200, "content-type": "application/json", "x-custom-header": "some-value", [http2.sensitiveHeaders]: ["x-custom-header"], }); stream.end(JSON.stringify({ message: "Hello from h2c server!" })); }); server.listen(0, () => { const port = server.address().port; client = http2.connect(`http://localhost:${port}`); client.on("error", reject); const req = client.request({ ":path": "/" }); req.on("response", resolve); req.on("error", reject); req.end(); }); const res = await promise; expect(res["x-custom-header"]).toBe("some-value"); expect(res[http2.sensitiveHeaders]).toEqual(["x-custom-header"]); } finally { server.close(); client?.close?.(); } }); it("http2 session.goaway() validates input types", async done => { const { mustCall } = createCallCheckCtx(done); const server = http2.createServer((req, res) => { res.end(); }); const types = [true, {}, [], null, new Date()]; return await new Promise(resolve => { server.on( "stream", mustCall(stream => { const session = stream.session; for (const input of types) { const received = invalidArgTypeHelper(input); // Test code argument expect(() => session.goaway(input)).toThrow('The "code" argument must be of type number.' + received); // Test lastStreamID argument expect(() => session.goaway(0, input)).toThrow( 'The "lastStreamID" argument must be of type number.' + received, ); // Test opaqueData argument expect(() => session.goaway(0, 0, input)).toThrow( 'The "opaqueData" argument must be of type Buffer, ' + `TypedArray, or DataView.${received}`, ); } server.close(); resolve(); }), ); server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); const req = client.request(); req.resume(); req.end(); }); }); }); it("http2 stream.close() validates input types and ranges", async () => { const server = http2.createServer(); return await new Promise(resolve => { server.on("stream", stream => { // Test string input expect(() => stream.close("string")).toThrow( 'The "code" argument must be of type number. ' + "Received type string ('string')", ); // Test non-integer number expect(() => stream.close(1.01)).toThrow( 'The value of "code" is out of range. It must be an integer. ' + "Received 1.01", ); // Test out of range values [-1, 2 ** 32].forEach(code => { expect(() => stream.close(code)).toThrow( `The value of "code" is out of range. It must be >= 0 and <= 4294967295. Received ${code}`, ); }); // Complete the stream stream.respond(); stream.end("ok"); }); server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); const req = client.request(); req.resume(); req.on("close", () => { server.close(); client.close(); resolve(); }); }); }); }); it("http2 session.goaway() sends custom data", async done => { const { mustCall } = createCallCheckCtx(done); const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); let session; const server = http2.createServer(); return await new Promise(resolve => { server.on("stream", stream => { session = stream.session; session.on("close", () => {}); // Send GOAWAY frame with custom data session.goaway(0, 0, data); // Complete the stream stream.respond(); stream.end(); }); server.on("close", mustCall()); server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); client.once("goaway", (code, lastStreamID, buf) => { // Verify the GOAWAY frame parameters expect(code).toBe(0); expect(lastStreamID).toBe(1); expect(buf).toEqual(data); // Clean up session.close(); server.close(); resolve(); }); const req = client.request(); req.resume(); req.on("end", mustCall()); req.on("close", mustCall()); req.end(); }); }); }); it( "http2 server with minimal maxSessionMemory handles multiple requests", async () => { const server = http2.createServer({ maxSessionMemory: 1 }); return await new Promise(resolve => { server.on("session", session => { session.on("stream", stream => { stream.on("end", function () { this.respond( { ":status": 200, }, { endStream: true, }, ); }); stream.resume(); }); }); server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); function next(i) { if (i === 10000) { client.close(); server.close(); resolve(); return; } const stream = client.request({ ":method": "POST" }); stream.on("response", function (headers) { expect(headers[":status"]).toBe(200); this.on("close", () => next(i + 1)); }); stream.end(); } // Start the sequence with the first request next(0); }); }); }, 15_000 * ASAN_MULTIPLIER, ); it("http2.createServer validates input options", () => { // Test invalid options passed to createServer const invalidOptions = [1, true, "test", null, Symbol("test")]; invalidOptions.forEach(invalidOption => { expect(() => http2.createServer(invalidOption)).toThrow( 'The "options" argument must be of type Object.' + invalidArgTypeHelper(invalidOption), ); }); // Test invalid options.settings passed to createServer invalidOptions.forEach(invalidSettingsOption => { expect(() => http2.createServer({ settings: invalidSettingsOption })).toThrow( 'The "options.settings" property must be of type Object.' + invalidArgTypeHelper(invalidSettingsOption), ); }); // Test that http2.createServer validates numeric range options const rangeTests = { maxSessionInvalidFrames: [ { val: -1, err: { name: "RangeError", code: "ERR_OUT_OF_RANGE", }, }, { val: Number.NEGATIVE_INFINITY, err: { name: "RangeError", code: "ERR_OUT_OF_RANGE", }, }, ], maxSessionRejectedStreams: [ { val: -1, err: { name: "RangeError", code: "ERR_OUT_OF_RANGE", }, }, { val: Number.NEGATIVE_INFINITY, err: { name: "RangeError", code: "ERR_OUT_OF_RANGE", }, }, ], }; Object.entries(rangeTests).forEach(([opt, tests]) => { tests.forEach(({ val, err }) => { expect(() => http2.createServer({ [opt]: val })).toThrow(); // Note: Bun's expect doesn't have the same detailed error matching as Node's assert, // so we're just checking that it throws an error with the expected name let error; try { http2.createServer({ [opt]: val }); } catch (e) { error = e; } expect(error).toBeTruthy(); expect(error?.name).toBe(err.name); expect(error?.code).toBe(err.code); }); }); }); it("http2 server handles multiple concurrent requests", async () => { const body = "

this is some data

"; const server = http2.createServer(); const count = 100; // Stream handler function onStream(stream, headers, flags) { expect(headers[":scheme"]).toBe("http"); expect(headers[":authority"]).toBeTruthy(); expect(headers[":method"]).toBe("GET"); expect(flags).toBe(5); stream.respond({ "content-type": "text/html", ":status": 200, }); stream.write(body.slice(0, 20)); stream.end(body.slice(20)); } // Register stream handler server.on("stream", (stream, headers, flags) => onStream(stream, headers, flags)); return await new Promise(resolve => { server.on("close", () => { resolve(); }); server.listen(0); server.on("listening", () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); client.setMaxListeners(101); client.on("goaway", console.log); client.on("connect", () => { expect(client.encrypted).toBeFalsy(); expect(client.originSet).toBeFalsy(); expect(client.alpnProtocol).toBe("h2c"); }); let countdown = count; function countDown() { countdown--; if (countdown === 0) { client.close(); server.close(); } } for (let n = 0; n < count; n++) { const req = client.request(); req.on("response", function (headers) { expect(headers[":status"]).toBe(200); expect(headers["content-type"]).toBe("text/html"); expect(headers.date).toBeTruthy(); }); let data = ""; req.setEncoding("utf8"); req.on("data", d => (data += d)); req.on("end", () => { expect(body).toBe(data); }); req.on("close", () => countDown()); } }); }); }); it("http2 connect supports various URL formats", async done => { const { mustCall } = createCallCheckCtx(done); return await new Promise(resolve => { const server = http2.createServer(); server.listen(0); server.on("listening", function () { const port = this.address().port; const items = [ [`http://localhost:${port}`], [new URL(`http://localhost:${port}`)], [{ protocol: "http:", hostname: "localhost", port }], [{ port }, { protocol: "http:" }], [{ port, hostname: "127.0.0.1" }, { protocol: "http:" }], ]; let countdown = items.length + 1; function countDown() { countdown--; if (countdown === 0) { setImmediate(() => { server.close(); resolve(); }); } } const maybeClose = client => { client.close(); countDown(); }; items.forEach(i => { const client = http2.connect.apply(null, i); client.on("connect", () => maybeClose(client)); client.on("close", mustCall()); }); // Will fail because protocol does not match the server. const client = http2.connect({ port: port, protocol: "https:", }); client.on("error", () => countDown()); client.on("close", mustCall()); }); }); }); it("http2 request.close() validates input and manages stream state", async done => { const { mustCall } = createCallCheckCtx(done); const server = http2.createServer(); server.on("stream", stream => { stream.on("close", () => {}); stream.respond(); stream.end("ok"); }); return await new Promise(resolve => { server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); const req = client.request(); const closeCode = 1; // Test out of range code expect(() => req.close(2 ** 32)).toThrow( 'The value of "code" is out of range. It must be ' + ">= 0 and <= 4294967295. Received 4294967296", ); expect(req.closed).toBe(false); // Test invalid callback argument types [true, 1, {}, [], null, "test"].forEach(notFunction => { expect(() => req.close(closeCode, notFunction)).toThrow(); expect(req.closed).toBe(false); }); // Valid close call with callback req.close(closeCode, mustCall()); expect(req.closed).toBe(true); // Store original _destroy method const originalDestroy = req._destroy; // Replace _destroy to check if it's called req._destroy = mustCall((...args) => { return originalDestroy.apply(req, args); }); // Second call doesn't do anything req.close(closeCode + 1); req.on("close", () => { expect(req.destroyed).toBe(true); expect(req.rstCode).toBe(closeCode); server.close(); client.close(); resolve(); }); req.on("error", err => { expect(err.code).toBe("ERR_HTTP2_STREAM_ERROR"); expect(err.name).toBe("Error"); expect(err.message).toBe("Stream closed with error code NGHTTP2_PROTOCOL_ERROR"); }); // The `response` event should not fire as the server should receive the // RST_STREAM frame before it ever has a chance to reply. req.on("response", () => { throw new Error("Response event should not be called"); }); // The `end` event should still fire as we close the readable stream by // pushing a `null` chunk. req.on("end", mustCall()); req.resume(); req.end(); }); }); }); it("http2 client.setNextStreamID validates input", async () => { const server = http2.createServer(); server.on("stream", stream => { stream.respond(); stream.end("ok"); }); const types = { boolean: true, function: () => {}, number: 1, object: {}, array: [], null: null, symbol: Symbol("test"), }; return await new Promise(resolve => { server.listen(0, () => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); client.on("connect", () => { // Test out of range value const outOfRangeNum = 2 ** 32; expect(() => client.setNextStreamID(outOfRangeNum)).toThrow( 'The value of "id" is out of range.' + " It must be > 0 and <= 4294967295. Received " + outOfRangeNum, ); // Test invalid types Object.entries(types).forEach(([type, value]) => { if (type === "number") { return; } try { client.setNextStreamID(value); // If we reach here, the function didn't throw, which is an error expect(false).toBe(true); // Force test failure } catch (err) { expect(err.name).toBe("TypeError"); expect(err.code).toBe("ERR_INVALID_ARG_TYPE"); expect(err.message).toContain('The "id" argument must be of type number'); } }); server.close(); client.close(); resolve(); }); }); }); }); it("http2 request.destroy() with error", async () => { const server = http2.createServer(); // Do not mustCall the server side callbacks, they may or may not be called // depending on the OS. The determination is based largely on operating // system specific timings server.on("stream", stream => { // Do not wrap in a must call or use common.expectsError (which now uses // must call). The error may or may not be reported depending on operating // system specific timings. stream.on("error", err => { expect(err.code).toBe("ERR_HTTP2_STREAM_ERROR"); expect(err.message).toBe("Stream closed with error code NGHTTP2_INTERNAL_ERROR"); }); stream.respond(); stream.end(); }); return new Promise(resolve => { server.listen(0, () => { let countdown = 2; function countDown() { countdown--; if (countdown === 0) { server.close(); client.close(); resolve(); } } const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); client.on("connect", () => countDown()); const req = client.request(); // Destroy the request with an error req.destroy(new Error("test")); // Error event should receive the provided error req.on("error", err => { expect(err.name).toBe("Error"); expect(err.message).toBe("test"); }); // Close event should fire with the correct reset code req.on("close", () => { expect(req.rstCode).toBe(http2.constants.NGHTTP2_INTERNAL_ERROR); countDown(); }); // These events should not fire since the stream is destroyed req.on("response", () => { throw new Error("response event should not be called"); }); req.resume(); req.on("end", () => { throw new Error("end event should not be called"); }); }); }); });