From f7b75cf2e220cee06268ede5dc4b263b9fdf1a64 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Mon, 12 May 2025 16:57:43 -0700 Subject: [PATCH] Revert "compat(http2) validations and some behavior fixes (#19558)" This reverts commit dd53294a25e43394600994320277cbcaa173b68c. It makes test-http2-client-upload.js fail in debug and flaky in release. --- src/bun.js/api/bun/h2_frame_parser.zig | 14 +- src/bun.js/api/h2.classes.ts | 4 - src/bun.js/bindings/ErrorCode.ts | 2 - src/bun.js/bindings/NodeValidator.cpp | 11 +- src/js/node/http2.ts | 261 ++------ test/js/node/http2/node-http2.test.js | 609 +----------------- ...t-http2-client-rststream-before-connect.js | 75 --- ...est-http2-client-setNextStreamID-errors.js | 61 -- ...p2-client-stream-destroy-before-connect.js | 53 -- ...est-http2-compat-write-head-after-close.js | 23 - .../node/test/parallel/test-http2-connect.js | 166 ----- .../test-http2-create-client-connect.js | 86 --- .../test-http2-create-client-session.js | 71 -- .../test-http2-createserver-options.js | 78 --- .../test-http2-forget-closed-streams.js | 53 -- .../parallel/test-http2-goaway-opaquedata.js | 36 -- .../test-http2-invalidargtypes-errors.js | 52 -- ...st-http2-server-shutdown-options-errors.js | 66 -- 18 files changed, 60 insertions(+), 1661 deletions(-) delete mode 100644 test/js/node/test/parallel/test-http2-client-rststream-before-connect.js delete mode 100644 test/js/node/test/parallel/test-http2-client-setNextStreamID-errors.js delete mode 100644 test/js/node/test/parallel/test-http2-client-stream-destroy-before-connect.js delete mode 100644 test/js/node/test/parallel/test-http2-compat-write-head-after-close.js delete mode 100644 test/js/node/test/parallel/test-http2-connect.js delete mode 100644 test/js/node/test/parallel/test-http2-create-client-connect.js delete mode 100644 test/js/node/test/parallel/test-http2-create-client-session.js delete mode 100644 test/js/node/test/parallel/test-http2-createserver-options.js delete mode 100644 test/js/node/test/parallel/test-http2-forget-closed-streams.js delete mode 100644 test/js/node/test/parallel/test-http2-goaway-opaquedata.js delete mode 100644 test/js/node/test/parallel/test-http2-invalidargtypes-errors.js delete mode 100644 test/js/node/test/parallel/test-http2-server-shutdown-options-errors.js diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index 104192cd37df..0640b10e9cc8 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -3041,6 +3041,9 @@ pub const H2FrameParser = struct { } const error_code = error_arg.toU32(); + if (error_code > 13) { + return globalObject.throw("Invalid ErrorCode", .{}); + } this.endStream(stream, @enumFromInt(error_code)); @@ -3469,16 +3472,6 @@ pub const H2FrameParser = struct { return stream_id; } - pub fn setNextStreamID(this: *H2FrameParser, _: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - JSC.markBinding(@src()); - const args_list = callframe.arguments(); - bun.debugAssert(args_list.len >= 1); - const stream_id_arg = args_list.ptr[0]; - bun.debugAssert(stream_id_arg.isNumber()); - this.lastStreamID = stream_id_arg.to(u32); - return .undefined; - } - pub fn hasNativeRead(this: *H2FrameParser, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { return JSC.JSValue.jsBoolean(this.native_socket == .tcp or this.native_socket == .tls); } @@ -3569,7 +3562,6 @@ pub const H2FrameParser = struct { } return .undefined; } - pub fn emitErrorToAllStreams(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { JSC.markBinding(@src()); diff --git a/src/bun.js/api/h2.classes.ts b/src/bun.js/api/h2.classes.ts index e45742fcbc1c..4ec452d29e42 100644 --- a/src/bun.js/api/h2.classes.ts +++ b/src/bun.js/api/h2.classes.ts @@ -101,10 +101,6 @@ export default [ fn: "hasNativeRead", length: 1, }, - setNextStreamID: { - fn: "setNextStreamID", - length: 1, - }, getAllStreams: { fn: "getAllStreams", length: 0, diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index 0a690e4b229f..0b20b27b2419 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -116,8 +116,6 @@ const errors: ErrorCodeMapping = [ ["ERR_HTTP2_STREAM_ERROR", Error], ["ERR_HTTP2_TRAILERS_ALREADY_SENT", Error], ["ERR_HTTP2_TRAILERS_NOT_READY", Error], - ["ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS", Error], - ["ERR_HTTP2_UNSUPPORTED_PROTOCOL", Error], ["ERR_ILLEGAL_CONSTRUCTOR", TypeError], ["ERR_INCOMPATIBLE_OPTION_PAIR", TypeError], ["ERR_INVALID_ADDRESS", Error], diff --git a/src/bun.js/bindings/NodeValidator.cpp b/src/bun.js/bindings/NodeValidator.cpp index 6a15574c4163..f141ec57446f 100644 --- a/src/bun.js/bindings/NodeValidator.cpp +++ b/src/bun.js/bindings/NodeValidator.cpp @@ -615,14 +615,11 @@ JSC_DEFINE_HOST_FUNCTION(jsFunction_validateBuffer, (JSC::JSGlobalObject * globa auto buffer = callFrame->argument(0); auto name = callFrame->argument(1); - if (!buffer.isUndefined()) { - if (!buffer.isCell()) return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Buffer, TypedArray, or DataView"_s, buffer); + if (!buffer.isCell()) return JSValue::encode(jsUndefined()); + auto ty = buffer.asCell()->type(); - auto ty = buffer.asCell()->type(); - - if (JSC::typedArrayType(ty) == NotTypedArray) { - return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Buffer, TypedArray, or DataView"_s, buffer); - } + if (JSC::typedArrayType(ty) == NotTypedArray) { + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, name, "Buffer, TypedArray, or DataView"_s, buffer); } return JSValue::encode(jsUndefined()); } diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index d67292d6b1cd..18e01137b215 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -1,34 +1,8 @@ // Hardcoded module "node:http2" -/* - * Portions of this code are derived from the Node.js project (https://nodejs.org/), - * originally developed by Node.js contributors and Joyent, Inc. - * - * Copyright Node.js contributors. All rights reserved. - * Copyright Joyent, Inc. and other Node contributors. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * Modifications were made to the original code. - */ + const { isTypedArray } = require("node:util/types"); const { hideFromStack, throwNotImplemented } = require("internal/shared"); -const { STATUS_CODES } = require("internal/http"); + const tls = require("node:tls"); const net = require("node:net"); const fs = require("node:fs"); @@ -38,7 +12,7 @@ const bunTLSConnectOptions = Symbol.for("::buntlsconnectoptions::"); const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::"); const kInfoHeaders = Symbol("sent-info-headers"); const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; -const MAX_ADDITIONAL_SETTINGS = 10; + const Stream = require("node:stream"); const { Readable } = Stream; type Http2ConnectOptions = { @@ -99,8 +73,6 @@ const kAppendHeader = Symbol("appendHeader"); const kAborted = Symbol("aborted"); const kRequest = Symbol("request"); const kHeadRequest = Symbol("headRequest"); -const kMaxStreams = 2 ** 32 - 1; -const kMaxInt = 4294967295; const { validateInteger, validateString, @@ -109,8 +81,6 @@ const { checkIsHttpToken, validateLinkHeaderValue, validateUint32, - validateBuffer, - validateNumber, } = require("internal/validators"); let utcCache; @@ -297,19 +267,7 @@ function assertValidHeader(name, value) { connectionHeaderMessageWarn(); } } -function assertIsObject(value: any, name: string, types?: string): asserts value is object { - if (value !== undefined && (!$isObject(value) || $isArray(value))) { - throw $ERR_INVALID_ARG_TYPE(name, types || "Object", value); - } -} -function assertIsArray(value: any, name: string, types?: string): asserts value is any[] { - if (value !== undefined && !$isArray(value)) { - throw $ERR_INVALID_ARG_TYPE(name, types || "Array", value); - } -} -hideFromStack(assertIsObject); -hideFromStack(assertIsArray); hideFromStack(assertValidHeader); class Http2ServerRequest extends Readable { @@ -1554,17 +1512,6 @@ type Settings = { class Http2Session extends EventEmitter { [bunHTTP2Socket]: TLSSocket | Socket | null; [bunHTTP2OriginSet]: Set | undefined = undefined; - [EventEmitter.captureRejectionSymbol](err, event, ...args) { - switch (event) { - case "stream": { - const stream = args[0]; - stream.destroy(err); - break; - } - default: - this.destroy(err); - } - } } function streamErrorFromCode(code: number) { @@ -1590,15 +1537,6 @@ function assertSession(session) { hideFromStack(assertSession); function pushToStream(stream, data) { - if (data && stream[bunHTTP2StreamStatus] & StreamState.Closed) { - if (!stream._readableState.ended) { - // closed, but not ended, so resume and push null to end the stream - stream.resume(); - stream.push(null); - } - return; - } - stream.push(data); } @@ -1629,10 +1567,7 @@ function markStreamClosed(stream: Http2Stream) { markWritableDone(stream); } } -function rstNextTick(id: number, rstCode: number) { - const session = this as Http2Session; - session[bunHTTP2Native]?.rstStream(id, rstCode); -} + class Http2Stream extends Duplex { #id: number; [bunHTTP2Session]: ClientHttp2Session | ServerHttp2Session | null = null; @@ -1785,21 +1720,21 @@ class Http2Stream extends Duplex { const session = this[bunHTTP2Session]; assertSession(session); code = code || 0; - validateInteger(code, "code", 0, kMaxInt); - - if (typeof callback !== "undefined") { - validateFunction(callback, "callback"); - this.once("close", callback); - } + validateInteger(code, "code", 0, 13); this.rstCode = code; markStreamClosed(this); session[bunHTTP2Native]?.rstStream(this.#id, code); } + + if (typeof callback === "function") { + this.once("close", callback); + } } _destroy(err, callback) { const { ending } = this._writableState; this.push(null); + if (!ending) { // If the writable side of the Http2Stream is still open, emit the // 'aborted' event and set the aborted flag. @@ -1830,23 +1765,10 @@ class Http2Stream extends Duplex { rstCode = this.rstCode = 0; } } - this.rstCode = rstCode; - // RST code 8 not emitted as an error as its used by clients to signify - // abort and is already covered by aborted event, also allows more - // seamless compatibility with http1 - if (err == null && rstCode !== NGHTTP2_NO_ERROR && rstCode !== NGHTTP2_CANCEL) - err = $ERR_HTTP2_STREAM_ERROR(nameForErrorCode[rstCode] || rstCode); markStreamClosed(this); + session[bunHTTP2Native]?.rstStream(this.#id, rstCode); this[bunHTTP2Session] = null; - // This notifies the session that this stream has been destroyed and - // gives the session the opportunity to clean itself up. The session - // will destroy if it has been closed and there are no other open or - // pending streams. Delay with setImmediate so we don't do it on the - // nghttp2 stack. - if (session) { - setImmediate(rstNextTick.bind(session, this.#id, rstCode)); - } callback(err); } @@ -1944,18 +1866,6 @@ class Http2Stream extends Duplex { callback(); } } - - [EventEmitter.captureRejectionSymbol](err, event, ...args) { - switch (event) { - case "stream": { - const stream = args[0]; - stream.destroy(err); - break; - } - default: - this.destroy(err); - } - } } class ClientHttp2Stream extends Http2Stream { constructor(streamId, session, headers) { @@ -2291,17 +2201,16 @@ class ServerHttp2Stream extends Http2Stream { options = { ...options, endStream: true }; endStream = true; } - const sendDate = options?.sendDate; - if (sendDate == null || sendDate) { - const current_date = headers["date"]; - if (current_date == null) { - headers["date"] = utcDate(); - } - } if (typeof options === "undefined") { session[bunHTTP2Native]?.request(this.id, undefined, headers, sensitiveNames); } else { + if (options.sendDate == null || options.sendDate) { + const current_date = headers["date"]; + if (current_date === null || current_date === undefined) { + headers["date"] = utcDate(); + } + } session[bunHTTP2Native]?.request(this.id, undefined, headers, sensitiveNames, options); } this.headersSent = true; @@ -2536,7 +2445,7 @@ class ServerHttp2Session extends Http2Session { sensitiveHeadersValue: string[] | undefined, flags: number, ) { - if (!self || typeof stream !== "object" || self.closed || stream.closed) return; + if (!self || typeof stream !== "object") return; const headers = toHeaderObject(rawheaders, sensitiveHeadersValue || []); if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { stream[kHeadRequest] = true; @@ -2846,15 +2755,8 @@ class ServerHttp2Session extends Http2Session { parser.ping(payload); return true; } - goaway(code = NGHTTP2_NO_ERROR, lastStreamID = 0, opaqueData) { - if (this.destroyed) throw $ERR_HTTP2_INVALID_SESSION(); - - if (opaqueData !== undefined) { - validateBuffer(opaqueData, "opaqueData"); - } - validateNumber(code, "code"); - validateNumber(lastStreamID, "lastStreamID"); - return this.#parser?.goaway(code, lastStreamID, opaqueData); + goaway(errorCode, lastStreamId, opaqueData) { + return this.#parser?.goaway(errorCode, lastStreamId, opaqueData); } setLocalWindowSize(windowSize) { @@ -2885,14 +2787,9 @@ class ServerHttp2Session extends Http2Session { } } - destroy(error: Error | number | undefined = NGHTTP2_NO_ERROR, code?: number) { - if (typeof error === "number") { - code = error; - error = code !== NGHTTP2_NO_ERROR ? $ERR_HTTP2_SESSION_ERROR(code) : undefined; - } - + destroy(error?: Error, code?: number) { const socket = this[bunHTTP2Socket]; - if (!this.#connected) return; + this.#closed = true; this.#connected = false; if (socket) { @@ -3007,7 +2904,7 @@ class ClientHttp2Session extends Http2Session { sensitiveHeadersValue: string[] | undefined, flags: number, ) { - if (!self || typeof stream !== "object" || self.closed || stream.closed) return; + if (!self || typeof stream !== "object") return; const headers = toHeaderObject(rawheaders, sensitiveHeadersValue || []); const status = stream[bunHTTP2StreamStatus]; const header_status = headers[HTTP2_HEADER_STATUS]; @@ -3076,8 +2973,10 @@ class ClientHttp2Session extends Http2Session { goaway(self: ClientHttp2Session, errorCode: number, lastStreamId: number, opaqueData: Buffer) { if (!self) return; self.emit("goaway", errorCode, lastStreamId, opaqueData || Buffer.allocUnsafe(0)); - if (self.closed) return; - self.destroy(undefined, errorCode); + if (errorCode !== 0) { + self.#parser.emitErrorToAllStreams(errorCode); + } + self.close(); }, end(self: ClientHttp2Session, errorCode: number, lastStreamId: number, opaqueData: Buffer) { if (!self) return; @@ -3226,13 +3125,6 @@ class ClientHttp2Session extends Http2Session { ref() { return this[bunHTTP2Socket]?.ref(); } - setNextStreamID(id) { - if (this.destroyed) throw $ERR_HTTP2_INVALID_SESSION(); - - validateNumber(id, "id"); - if (id <= 0 || id > kMaxStreams) throw $ERR_OUT_OF_RANGE("id", `> 0 and <= ${kMaxStreams}`, id); - this.#parser?.setNextStreamID(id); - } setTimeout(msecs, callback) { return this[bunHTTP2Socket]?.setTimeout(msecs, callback); } @@ -3300,34 +3192,19 @@ class ClientHttp2Session extends Http2Session { constructor(url: string | URL, options?: Http2ConnectOptions, listener?: Function) { super(); + if (typeof url === "string") { + url = new URL(url); + } + if (!(url instanceof URL)) { + throw $ERR_INVALID_ARG_TYPE("url", "URL", url); + } if (typeof options === "function") { listener = options; options = undefined; } - - assertIsObject(options, "options"); - options = { ...options }; - - assertIsArray(options.remoteCustomSettings, "options.remoteCustomSettings"); - if (options.remoteCustomSettings) { - options.remoteCustomSettings = [...options.remoteCustomSettings]; - if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) throw $ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); - } - - if (typeof url === "string") url = new URL(url); - - assertIsObject(url, "authority", ["string", "Object", "URL"]); - this.#url = url; const protocol = url.protocol || options?.protocol || "https:"; - switch (protocol) { - case "http:": - case "https:": - break; - default: - throw $ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol); - } const port = url.port ? parseInt(url.port, 10) : protocol === "http:" ? 80 : 443; function onConnect() { @@ -3597,48 +3474,17 @@ function connectionListener(socket: Socket) { } } } - -function initializeOptions(options) { - assertIsObject(options, "options"); - options = { ...options }; - assertIsObject(options.settings, "options.settings"); - options.settings = { ...options.settings }; - - assertIsArray(options.remoteCustomSettings, "options.remoteCustomSettings"); - if (options.remoteCustomSettings) { - options.remoteCustomSettings = [...options.remoteCustomSettings]; - if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) throw $ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); - } - - if (options.maxSessionInvalidFrames !== undefined) - validateUint32(options.maxSessionInvalidFrames, "maxSessionInvalidFrames"); - - if (options.maxSessionRejectedStreams !== undefined) { - validateUint32(options.maxSessionRejectedStreams, "maxSessionRejectedStreams"); - } - - if (options.unknownProtocolTimeout !== undefined) - validateUint32(options.unknownProtocolTimeout, "unknownProtocolTimeout"); - else options.unknownProtocolTimeout = 10000; - - // Used only with allowHTTP1 - // options.Http1IncomingMessage ||= http.IncomingMessage; - // options.Http1ServerResponse ||= http.ServerResponse; - - options.Http2ServerRequest ||= Http2ServerRequest; - options.Http2ServerResponse ||= Http2ServerResponse; - return options; -} - class Http2Server extends net.Server { timeout = 0; constructor(options, onRequestHandler) { if (typeof options === "function") { onRequestHandler = options; options = {}; + } else if (options == null || typeof options == "object") { + options = { ...options }; + } else { + throw $ERR_INVALID_ARG_TYPE("options", "object", options); } - options = initializeOptions(options); - super(options, connectionListener); this.setMaxListeners(0); @@ -3663,39 +3509,6 @@ class Http2Server extends net.Server { } } -Http2Server.prototype[EventEmitter.captureRejectionSymbol] = function (err, event, ...args) { - switch (event) { - case "stream": { - const { 0: stream } = args; - if (stream.sentHeaders) { - stream.destroy(err); - } else { - stream.respond({ [HTTP2_HEADER_STATUS]: 500 }); - stream.end(); - } - break; - } - case "request": { - const { 1: res } = args; - if (!res.headersSent && !res.finished) { - // Don't leak headers. - for (const name of res.getHeaderNames()) { - res.removeHeader(name); - } - res.statusCode = 500; - res.end(STATUS_CODES[500]); - } else { - res.destroy(); - } - break; - } - default: - // args.unshift(err, event); - // ReflectApply(net.Server.prototype[EventEmitter.captureRejectionSymbol], this, args); - break; - } -}; - function onErrorSecureServerSession(err, socket) { if (!this.emit("clientError", err, socket)) socket.destroy(err); } diff --git a/test/js/node/http2/node-http2.test.js b/test/js/node/http2/node-http2.test.js index 479bf8d3ef70..8c9042d46e29 100644 --- a/test/js/node/http2/node-http2.test.js +++ b/test/js/node/http2/node-http2.test.js @@ -1,5 +1,5 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { bunEnv, bunExe, isCI, nodeExe } from "harness"; -import { createTest } from "node-harness"; import fs from "node:fs"; import http2 from "node:http2"; import net from "node:net"; @@ -9,16 +9,7 @@ import tls from "node:tls"; import { Duplex } from "stream"; import http2utils from "./helpers"; import { nodeEchoServer, TLS_CERT, TLS_OPTIONS } from "./http2-helpers"; -const { afterEach, beforeEach, describe, expect, it, createCallCheckCtx } = createTest(import.meta.path); -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})`; -} for (const nodeExecutable of [nodeExe(), bunExe()]) { describe(`${path.basename(nodeExecutable)}`, () => { let nodeEchoServer_; @@ -908,7 +899,21 @@ for (const nodeExecutable of [nodeExe(), bunExe()]) { expect(stream).toBeDefined(); expect(stream.id).toBe(1); }); - + it("should wait request to be sent before closing", async () => { + const { promise, resolve, reject } = Promise.withResolvers(); + const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); + client.on("error", reject); + const req = client.request({ ":path": "/" }); + let response_headers = null; + req.on("response", (headers, flags) => { + response_headers = headers; + }); + client.close(resolve); + req.end(); + await promise; + expect(response_headers).toBeTruthy(); + expect(response_headers[":status"]).toBe(200); + }); it("wantTrailers should work", async () => { const { promise, resolve, reject } = Promise.withResolvers(); const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS); @@ -1329,585 +1334,3 @@ it("sensitive headers should work", async () => { 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); - }); - }); -}, 10_000); - -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"); - }); - }); - }); -}); diff --git a/test/js/node/test/parallel/test-http2-client-rststream-before-connect.js b/test/js/node/test/parallel/test-http2-client-rststream-before-connect.js deleted file mode 100644 index 04fd1b1d2ae4..000000000000 --- a/test/js/node/test/parallel/test-http2-client-rststream-before-connect.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const h2 = require('http2'); - -const server = h2.createServer(); -server.on('stream', (stream) => { - stream.on('close', common.mustCall()); - stream.respond(); - stream.end('ok'); -}); - -server.listen(0, common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - const closeCode = 1; - - assert.throws( - () => req.close(2 ** 32), - { - name: 'RangeError', - code: 'ERR_OUT_OF_RANGE', - message: 'The value of "code" is out of range. It must be ' + - '>= 0 and <= 4294967295. Received 4294967296' - } - ); - assert.strictEqual(req.closed, false); - - [true, 1, {}, [], null, 'test'].forEach((notFunction) => { - assert.throws( - () => req.close(closeCode, notFunction), - { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - } - ); - assert.strictEqual(req.closed, false); - }); - - req.close(closeCode, common.mustCall()); - assert.strictEqual(req.closed, true); - - // Make sure that destroy is called. - req._destroy = common.mustCall(req._destroy.bind(req)); - - // Second call doesn't do anything. - req.close(closeCode + 1); - - req.on('close', common.mustCall(() => { - assert.strictEqual(req.destroyed, true); - assert.strictEqual(req.rstCode, closeCode); - server.close(); - client.close(); - })); - - req.on('error', common.expectsError({ - code: 'ERR_HTTP2_STREAM_ERROR', - name: 'Error', - message: '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', common.mustNotCall()); - - // The `end` event should still fire as we close the readable stream by - // pushing a `null` chunk. - req.on('end', common.mustCall()); - - req.resume(); - req.end(); -})); - diff --git a/test/js/node/test/parallel/test-http2-client-setNextStreamID-errors.js b/test/js/node/test/parallel/test-http2-client-setNextStreamID-errors.js deleted file mode 100644 index aace5845252a..000000000000 --- a/test/js/node/test/parallel/test-http2-client-setNextStreamID-errors.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const assert = require('assert'); -const http2 = require('http2'); - -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') -}; - -server.listen(0, common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}`); - - client.on('connect', () => { - const outOfRangeNum = 2 ** 32; - assert.throws( - () => client.setNextStreamID(outOfRangeNum), - { - name: 'RangeError', - code: 'ERR_OUT_OF_RANGE', - message: 'The value of "id" is out of range.' + - ' It must be > 0 and <= 4294967295. Received ' + outOfRangeNum - } - ); - - // Should throw if something other than number is passed to setNextStreamID - Object.entries(types).forEach(([type, value]) => { - if (type === 'number') { - return; - } - - assert.throws( - () => client.setNextStreamID(value), - { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "id" argument must be of type number.' + - common.invalidArgTypeHelper(value) - } - ); - }); - - server.close(); - client.close(); - }); -})); diff --git a/test/js/node/test/parallel/test-http2-client-stream-destroy-before-connect.js b/test/js/node/test/parallel/test-http2-client-stream-destroy-before-connect.js deleted file mode 100644 index 5d059b7acd3b..000000000000 --- a/test/js/node/test/parallel/test-http2-client-stream-destroy-before-connect.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const h2 = require('http2'); -const NGHTTP2_INTERNAL_ERROR = h2.constants.NGHTTP2_INTERNAL_ERROR; -const Countdown = require('../common/countdown'); - -const server = h2.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) => { - assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); - assert.strictEqual(err.message, - 'Stream closed with error code NGHTTP2_INTERNAL_ERROR'); - }); - stream.respond(); - stream.end(); -}); - -server.listen(0, common.mustCall(() => { - const client = h2.connect(`http://localhost:${server.address().port}`); - const countdown = new Countdown(2, () => { - server.close(); - client.close(); - }); - client.on('connect', () => countdown.dec()); - - const req = client.request(); - req.destroy(new Error('test')); - - req.on('error', common.expectsError({ - name: 'Error', - message: 'test' - })); - - req.on('close', common.mustCall(() => { - assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR); - countdown.dec(); - })); - - req.on('response', common.mustNotCall()); - req.resume(); - req.on('end', common.mustNotCall()); -})); diff --git a/test/js/node/test/parallel/test-http2-compat-write-head-after-close.js b/test/js/node/test/parallel/test-http2-compat-write-head-after-close.js deleted file mode 100644 index 541973f5dbc5..000000000000 --- a/test/js/node/test/parallel/test-http2-compat-write-head-after-close.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) { common.skip('missing crypto'); }; -const h2 = require('http2'); - -const server = h2.createServer((req, res) => { - const stream = req.stream; - stream.close(); - res.writeHead(200, { 'content-type': 'text/plain' }); -}); - -server.listen(0, common.mustCall(() => { - const port = server.address().port; - const client = h2.connect(`http://localhost:${port}`); - const req = client.request({ ':path': '/' }); - req.on('response', common.mustNotCall('head after close should not be sent')); - req.on('end', common.mustCall(() => { - client.close(); - server.close(); - })); - req.end(); -})); diff --git a/test/js/node/test/parallel/test-http2-connect.js b/test/js/node/test/parallel/test-http2-connect.js deleted file mode 100644 index 9b4055373f25..000000000000 --- a/test/js/node/test/parallel/test-http2-connect.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -const { - mustCall, - hasCrypto, - hasIPv6, - skip, - expectsError -} = require('../common'); -if (!hasCrypto) - skip('missing crypto'); -const fixtures = require('../common/fixtures'); -const assert = require('assert'); -const { createServer, createSecureServer, connect } = require('http2'); -const { connect: netConnect } = require('net'); -const { connect: tlsConnect } = require('tls'); - -// Check for session connect callback and event -{ - const server = createServer(); - server.listen(0, mustCall(() => { - const authority = `http://localhost:${server.address().port}`; - const options = {}; - const listener = () => mustCall(); - - const clients = new Set(); - // Should not throw. - clients.add(connect(authority)); - clients.add(connect(authority, options)); - clients.add(connect(authority, options, listener())); - clients.add(connect(authority, listener())); - - for (const client of clients) { - client.once('connect', mustCall((headers) => { - client.close(); - clients.delete(client); - if (clients.size === 0) { - server.close(); - } - })); - } - })); -} - -// Check for session connect callback on already connected socket -{ - const server = createServer(); - server.listen(0, mustCall(() => { - const { port } = server.address(); - - const onSocketConnect = () => { - const authority = `http://localhost:${port}`; - const createConnection = mustCall(() => socket); - const options = { createConnection }; - connect(authority, options, mustCall(onSessionConnect)); - }; - - const onSessionConnect = (session) => { - session.close(); - server.close(); - }; - - const socket = netConnect(port, mustCall(onSocketConnect)); - })); -} - -// Check for https as protocol -{ - const authority = 'https://localhost'; - // A socket error may or may not be reported, keep this as a non-op - // instead of a mustCall or mustNotCall - connect(authority).on('error', () => {}); -} - -// Check for session connect callback on already connected TLS socket -{ - const serverOptions = { - key: fixtures.readKey('agent1-key.pem'), - cert: fixtures.readKey('agent1-cert.pem') - }; - const server = createSecureServer(serverOptions); - server.listen(0, mustCall(() => { - const { port } = server.address(); - - const onSocketConnect = () => { - const authority = `https://localhost:${port}`; - const createConnection = mustCall(() => socket); - const options = { createConnection }; - connect(authority, options, mustCall(onSessionConnect)); - }; - - const onSessionConnect = (session) => { - session.close(); - server.close(); - }; - - const clientOptions = { - ALPNProtocols: ['h2'], - port, - rejectUnauthorized: false - }; - const socket = tlsConnect(clientOptions, mustCall(onSocketConnect)); - })); -} - -// Check for error for init settings error -{ - createServer(function() { - connect(`http://localhost:${this.address().port}`, { - settings: { - maxFrameSize: 1 // An incorrect settings - } - }).on('error', expectsError({ - code: 'ERR_HTTP2_INVALID_SETTING_VALUE', - name: 'RangeError' - })); - }); -} - -// Check for error for an invalid protocol (not http or https) -{ - const authority = 'ssh://localhost'; - assert.throws(() => { - connect(authority); - }, { - code: 'ERR_HTTP2_UNSUPPORTED_PROTOCOL', - name: 'Error' - }); -} - -// Check for literal IPv6 addresses in URL's -if (hasIPv6) { - const server = createServer(); - server.listen(0, '::1', mustCall(() => { - const { port } = server.address(); - const clients = new Set(); - - clients.add(connect(`http://[::1]:${port}`)); - clients.add(connect(new URL(`http://[::1]:${port}`))); - - for (const client of clients) { - client.once('connect', mustCall(() => { - client.close(); - clients.delete(client); - if (clients.size === 0) { - server.close(); - } - })); - } - })); -} - -// Check that `options.host` and `options.port` take precedence over -// `authority.host` and `authority.port`. -{ - const server = createServer(); - server.listen(0, mustCall(() => { - connect('http://foo.bar', { - host: 'localhost', - port: server.address().port - }, mustCall((session) => { - session.close(); - server.close(); - })); - })); -} diff --git a/test/js/node/test/parallel/test-http2-create-client-connect.js b/test/js/node/test/parallel/test-http2-create-client-connect.js deleted file mode 100644 index 9859fe106771..000000000000 --- a/test/js/node/test/parallel/test-http2-create-client-connect.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -// Tests http2.connect() - -const common = require('../common'); -const Countdown = require('../common/countdown'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const fixtures = require('../common/fixtures'); -const h2 = require('http2'); -const url = require('url'); - -{ - const server = h2.createServer(); - server.listen(0); - - server.on('listening', common.mustCall(function() { - const port = this.address().port; - - const items = [ - [`http://localhost:${port}`], - [new URL(`http://localhost:${port}`)], - [url.parse(`http://localhost:${port}`)], - [{ port }, { protocol: 'http:' }], - [{ port, hostname: '127.0.0.1' }, { protocol: 'http:' }], - ]; - - const serverClose = new Countdown(items.length + 1, - () => setImmediate(() => server.close())); - - const maybeClose = common.mustCall((client) => { - client.close(); - serverClose.dec(); - }, items.length); - - items.forEach((i) => { - const client = - h2.connect.apply(null, i) - .on('connect', common.mustCall(() => maybeClose(client))); - client.on('close', common.mustCall()); - }); - - // Will fail because protocol does not match the server. - const client = h2.connect({ port: port, protocol: 'https:' }) - .on('error', common.mustCall(() => serverClose.dec())); - client.on('close', common.mustCall()); - })); -} - - -{ - - const options = { - key: fixtures.readKey('agent3-key.pem'), - cert: fixtures.readKey('agent3-cert.pem') - }; - - const server = h2.createSecureServer(options); - server.listen(0, common.mustCall(() => { - const port = server.address().port; - - const opts = { rejectUnauthorized: false }; - - const items = [ - [`https://localhost:${port}`, opts], - [new URL(`https://localhost:${port}`), opts], - [url.parse(`https://localhost:${port}`), opts], - [{ port: port, protocol: 'https:' }, opts], - [{ port: port, hostname: '127.0.0.1', protocol: 'https:' }, opts], - ]; - - const serverClose = new Countdown(items.length, - () => setImmediate(() => server.close())); - - const maybeClose = common.mustCall((client) => { - client.close(); - serverClose.dec(); - }, items.length); - - items.forEach((i) => { - const client = - h2.connect.apply(null, i) - .on('connect', common.mustCall(() => maybeClose(client))); - }); - })); -} diff --git a/test/js/node/test/parallel/test-http2-create-client-session.js b/test/js/node/test/parallel/test-http2-create-client-session.js deleted file mode 100644 index b2d6ddbe7a03..000000000000 --- a/test/js/node/test/parallel/test-http2-create-client-session.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const h2 = require('http2'); -const Countdown = require('../common/countdown'); - -const body = - '

this is some data

'; - -const server = h2.createServer(); -const count = 100; - -// We use the lower-level API here -server.on('stream', common.mustCall(onStream, count)); - -function onStream(stream, headers, flags) { - assert.strictEqual(headers[':scheme'], 'http'); - assert.ok(headers[':authority']); - assert.strictEqual(headers[':method'], 'GET'); - assert.strictEqual(flags, 5); - stream.respond({ - 'content-type': 'text/html', - ':status': 200 - }); - stream.write(body.slice(0, 20)); - stream.end(body.slice(20)); -} - -server.on('close', common.mustCall()); - -server.listen(0); - -server.on('listening', common.mustCall(() => { - - const client = h2.connect(`http://localhost:${server.address().port}`); - client.setMaxListeners(101); - - client.on('goaway', console.log); - - client.on('connect', common.mustCall(() => { - assert(!client.encrypted); - assert(!client.originSet); - assert.strictEqual(client.alpnProtocol, 'h2c'); - })); - - const countdown = new Countdown(count, () => { - client.close(); - server.close(common.mustCall()); - }); - - for (let n = 0; n < count; n++) { - const req = client.request(); - - req.on('response', common.mustCall(function(headers) { - assert.strictEqual(headers[':status'], 200); - assert.strictEqual(headers['content-type'], 'text/html'); - assert(headers.date); - })); - - let data = ''; - req.setEncoding('utf8'); - req.on('data', (d) => data += d); - req.on('end', common.mustCall(() => { - assert.strictEqual(body, data); - })); - req.on('close', common.mustCall(() => countdown.dec())); - } -})); diff --git a/test/js/node/test/parallel/test-http2-createserver-options.js b/test/js/node/test/parallel/test-http2-createserver-options.js deleted file mode 100644 index 258e1fc39676..000000000000 --- a/test/js/node/test/parallel/test-http2-createserver-options.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const assert = require('assert'); -const http2 = require('http2'); - -// Error if invalid options are passed to createServer. -const invalidOptions = [1, true, 'test', null, Symbol('test')]; -invalidOptions.forEach((invalidOption) => { - assert.throws( - () => http2.createServer(invalidOption), - { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "options" argument must be of type Object.' + - common.invalidArgTypeHelper(invalidOption) - } - ); -}); - -// Error if invalid options.settings are passed to createServer. -invalidOptions.forEach((invalidSettingsOption) => { - assert.throws( - () => http2.createServer({ settings: invalidSettingsOption }), - { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "options.settings" property must be of type Object.' + - common.invalidArgTypeHelper(invalidSettingsOption) - } - ); -}); - -// Test that http2.createServer validates input options. -Object.entries({ - 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', - }, - }, - ] -}).forEach(([opt, tests]) => { - tests.forEach(({ val, err }) => { - assert.throws( - () => http2.createServer({ [opt]: val }), - err - ); - }); -}); diff --git a/test/js/node/test/parallel/test-http2-forget-closed-streams.js b/test/js/node/test/parallel/test-http2-forget-closed-streams.js deleted file mode 100644 index c0b3bcd81914..000000000000 --- a/test/js/node/test/parallel/test-http2-forget-closed-streams.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasCrypto) { - common.skip('missing crypto'); -} - -// Issue #23116 -// nghttp2 keeps closed stream structures around in memory (couple of hundred -// bytes each) until a session is closed. It does this to maintain the priority -// tree. However, it limits the number of requests that can be made in a -// session before our memory tracking (correctly) kicks in. -// The fix is to tell nghttp2 to forget about closed streams. We don't make use -// of priority anyway. -// Without the fix, this test fails at ~40k requests with an exception: -// Error [ERR_HTTP2_STREAM_ERROR]: Stream closed with error code -// NGHTTP2_ENHANCE_YOUR_CALM - -const http2 = require('http2'); -const assert = require('assert'); - -const server = http2.createServer({ maxSessionMemory: 1 }); - -server.on('session', function(session) { - session.on('stream', function(stream) { - stream.on('end', common.mustCall(function() { - this.respond({ - ':status': 200 - }, { - endStream: true - }); - })); - stream.resume(); - }); -}); - -server.listen(0, function() { - const client = http2.connect(`http://localhost:${server.address().port}`); - - function next(i) { - if (i === 10000) { - client.close(); - return server.close(); - } - const stream = client.request({ ':method': 'POST' }); - stream.on('response', common.mustCall(function(headers) { - assert.strictEqual(headers[':status'], 200); - this.on('close', common.mustCall(() => next(i + 1))); - })); - stream.end(); - } - - next(0); -}); diff --git a/test/js/node/test/parallel/test-http2-goaway-opaquedata.js b/test/js/node/test/parallel/test-http2-goaway-opaquedata.js deleted file mode 100644 index 538522883a1c..000000000000 --- a/test/js/node/test/parallel/test-http2-goaway-opaquedata.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const http2 = require('http2'); - -const server = http2.createServer(); -const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]); -let session; - -server.on('stream', common.mustCall((stream) => { - session = stream.session; - session.on('close', common.mustCall()); - session.goaway(0, 0, data); - stream.respond(); - stream.end(); -})); -server.on('close', common.mustCall()); - -server.listen(0, () => { - const client = http2.connect(`http://localhost:${server.address().port}`); - client.once('goaway', common.mustCall((code, lastStreamID, buf) => { - assert.strictEqual(code, 0); - assert.strictEqual(lastStreamID, 1); - assert.deepStrictEqual(data, buf); - session.close(); - server.close(); - })); - const req = client.request(); - req.resume(); - req.on('end', common.mustCall()); - req.on('close', common.mustCall()); - req.end(); -}); diff --git a/test/js/node/test/parallel/test-http2-invalidargtypes-errors.js b/test/js/node/test/parallel/test-http2-invalidargtypes-errors.js deleted file mode 100644 index b0d32ec87c33..000000000000 --- a/test/js/node/test/parallel/test-http2-invalidargtypes-errors.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const http2 = require('http2'); - -const server = http2.createServer(); - -server.on('stream', common.mustCall((stream) => { - assert.throws( - () => stream.close('string'), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "code" argument must be of type number. ' + - "Received type string ('string')" - } - ); - assert.throws( - () => stream.close(1.01), - { - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: 'The value of "code" is out of range. It must be an integer. ' + - 'Received 1.01' - } - ); - [-1, 2 ** 32].forEach((code) => { - assert.throws( - () => stream.close(code), - { - code: 'ERR_OUT_OF_RANGE', - name: 'RangeError', - message: `The value of "code" is out of range. It must be >= 0 and <= 4294967295. Received ${code}`, - } - ); - }); - stream.respond(); - stream.end('ok'); -})); - -server.listen(0, common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - req.resume(); - req.on('close', common.mustCall(() => { - server.close(); - client.close(); - })); -})); diff --git a/test/js/node/test/parallel/test-http2-server-shutdown-options-errors.js b/test/js/node/test/parallel/test-http2-server-shutdown-options-errors.js deleted file mode 100644 index 364b43b36b1e..000000000000 --- a/test/js/node/test/parallel/test-http2-server-shutdown-options-errors.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const assert = require('assert'); -const http2 = require('http2'); - -const server = http2.createServer(); - -const types = [ - true, - {}, - [], - null, - new Date(), -]; - -server.on('stream', common.mustCall((stream) => { - const session = stream.session; - - for (const input of types) { - const received = common.invalidArgTypeHelper(input); - assert.throws( - () => session.goaway(input), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "code" argument must be of type number.' + - received - } - ); - assert.throws( - () => session.goaway(0, input), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "lastStreamID" argument must be of type number.' + - received - } - ); - assert.throws( - () => session.goaway(0, 0, input), - { - code: 'ERR_INVALID_ARG_TYPE', - name: 'TypeError', - message: 'The "opaqueData" argument must be of type Buffer, ' + - `TypedArray, or DataView.${received}` - } - ); - } - stream.session.destroy(); -})); - -server.listen( - 0, - common.mustCall(() => { - const client = http2.connect(`http://localhost:${server.address().port}`); - const req = client.request(); - req.resume(); - req.on('close', common.mustCall(() => { - client.close(); - server.close(); - })); - }) -);