// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-console import * as http2 from "node:http2"; import { Buffer } from "node:buffer"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import * as net from "node:net"; import { assert, assertEquals } from "@std/assert"; import { curlRequest } from "../unit/test_util.ts"; // Increase the timeout for the auto select family to avoid flakiness net.setDefaultAutoSelectFamilyAttemptTimeout( net.getDefaultAutoSelectFamilyAttemptTimeout() * 30, ); for (const url of ["http://localhost:4246", "https://localhost:4247"]) { Deno.test(`[node/http2 client] ${url}`, { ignore: Deno.build.os === "windows", }, async () => { // Create a server to respond to the HTTP2 requests const client = http2.connect(url, {}); client.on("error", (err) => console.error(err)); const req = client.request({ ":method": "POST", ":path": "/" }, { waitForTrailers: true, }); let receivedTrailers; let receivedHeaders; let receivedData = ""; req.on("response", (headers, _flags) => { receivedHeaders = headers; }); req.write("hello"); req.setEncoding("utf8"); req.on("wantTrailers", () => { req.sendTrailers({ foo: "bar" }); }); req.on("trailers", (trailers, _flags) => { receivedTrailers = trailers; }); req.on("data", (chunk) => { receivedData += chunk; }); req.end(); const { promise, resolve } = Promise.withResolvers(); setTimeout(() => { try { client.close(); } catch (_) { // pass } resolve(); }, 2000); await promise; assertEquals(receivedHeaders, { ":status": 200 }); assertEquals(receivedData, "hello world\n"); assertEquals(receivedTrailers, { "abc": "def", "opr": "stv", "foo": "bar", }); }); } Deno.test(`[node/http2 client createConnection]`, { ignore: Deno.build.os === "windows", }, async () => { const url = "http://127.0.0.1:4246"; const createConnDeferred = Promise.withResolvers(); // Create a server to respond to the HTTP2 requests const client = http2.connect(url, { createConnection() { const socket = net.connect({ host: "127.0.0.1", port: 4246 }); socket.on("connect", () => { createConnDeferred.resolve(); }); return socket; }, }); client.on("error", (err) => console.error(err)); const req = client.request({ ":method": "POST", ":path": "/" }); let receivedData = ""; req.write("hello"); req.setEncoding("utf8"); req.on("data", (chunk) => { receivedData += chunk; }); req.end(); const endPromise = Promise.withResolvers(); setTimeout(() => { try { client.close(); } catch (_) { // pass } endPromise.resolve(); }, 2000); await createConnDeferred.promise; await endPromise.promise; assertEquals(receivedData, "hello world\n"); }); Deno.test("[node/http2 client GET https://www.example.com]", async () => { const clientSession = http2.connect("https://www.example.com"); const req = clientSession.request({ ":method": "GET", ":path": "/", }); let headers = {}; let status: number | undefined = 0; let chunk = new Uint8Array(); const endPromise = Promise.withResolvers(); req.on("response", (h) => { status = h[":status"]; headers = h; }); req.on("data", (c) => { chunk = c; }); req.on("end", () => { clientSession.close(); req.close(); endPromise.resolve(); }); req.end(); await endPromise.promise; assert(Object.keys(headers).length > 0); assertEquals(status, 200); assert(chunk.length > 0); }); Deno.test("[node/http2.createServer()]", { // TODO(satyarohith): enable the test on windows. ignore: Deno.build.os === "windows", }, async () => { const serverListening = Promise.withResolvers(); const server = http2.createServer((_req, res) => { res.setHeader("Content-Type", "text/html"); res.setHeader("X-Foo", "bar"); res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); res.write("Hello, World!"); res.end(); }); server.listen(0, () => { serverListening.resolve((server.address() as net.AddressInfo).port); }); const port = await serverListening.promise; const endpoint = `http://localhost:${port}`; const response = await curlRequest([ endpoint, "--http2-prior-knowledge", ]); assertEquals(response, "Hello, World!"); server.close(); // Wait to avoid leaking the timer from here // https://github.com/denoland/deno/blob/749b6e45e58ac87188027f79fe403d130f86bd73/ext/node/polyfills/net.ts#L2396-L2402 // Issue: https://github.com/denoland/deno/issues/22764 await new Promise((resolve) => server.on("close", resolve)); }); Deno.test("[node/http2 client] write image buffer on request stream works", async () => { const url = "https://localhost:5545"; const client = http2.connect(url); client.on("error", (err) => console.error(err)); const imagePath = join(import.meta.dirname!, "testdata", "green.jpg"); const buffer = await readFile(imagePath); const req = client.request({ ":method": "POST", ":path": "/echo_server" }); req.write(buffer, (err) => { if (err) throw err; }); let receivedData: Buffer; req.on("data", (chunk) => { if (!receivedData) { receivedData = chunk; } else { receivedData = Buffer.concat([receivedData, chunk]); } }); req.end(); const endPromise = Promise.withResolvers(); setTimeout(() => { try { client.close(); } catch (_) { // pass } endPromise.resolve(); }, 2000); await endPromise.promise; assertEquals(receivedData!, buffer); }); Deno.test("[node/http2 client] write 512kb buffer on request stream works", async () => { const url = "https://localhost:5545"; const client = http2.connect(url); client.on("error", (err) => console.error(err)); const filePath = join( import.meta.dirname!, "testdata", "lorem_ipsum_512kb.txt", ); const buffer = await readFile(filePath); const req = client.request({ ":method": "POST", ":path": "/echo_server" }); req.write(buffer, (err) => { if (err) throw err; }); let receivedData: Buffer; req.on("data", (chunk) => { if (!receivedData) { receivedData = chunk; } else { receivedData = Buffer.concat([receivedData, chunk]); } }); req.end(); const endPromise = Promise.withResolvers(); setTimeout(() => { try { client.close(); } catch (_) { // pass } endPromise.resolve(); }, 2000); await endPromise.promise; assertEquals(receivedData!, buffer); }); // https://github.com/denoland/deno/issues/24678 Deno.test("[node/http2 client] deno doesn't panic on uppercase headers", async () => { const url = "http://127.0.0.1:4246"; const client = http2.connect(url); client.on("error", (err) => console.error(err)); // The "User-Agent" header has uppercase characters to test the panic. const req = client.request({ ":method": "POST", ":path": "/", "User-Agent": "http2", }); const endPromise = Promise.withResolvers(); let receivedData = ""; req.write("hello"); req.setEncoding("utf8"); req.on("data", (chunk) => { receivedData += chunk; }); req.on("end", () => { req.close(); client.close(); endPromise.resolve(); }); req.end(); await endPromise.promise; assertEquals(receivedData, "hello world\n"); }); Deno.test("[node/http2 ClientHttp2Session.socket]", async () => { const url = "http://127.0.0.1:4246"; const client = http2.connect(url); client.on("error", (err) => console.error(err)); const req = client.request({ ":method": "POST", ":path": "/" }); const endPromise = Promise.withResolvers(); // test that we can access session.socket client.socket.setTimeout(10000); // nodejs allows setting arbitrary properties // deno-lint-ignore no-explicit-any (client.socket as any).nonExistant = 9001; // deno-lint-ignore no-explicit-any assertEquals((client.socket as any).nonExistant, 9001); // regular request dance to make sure it keeps working let receivedData = ""; req.write("hello"); req.setEncoding("utf8"); req.on("data", (chunk) => { receivedData += chunk; }); req.on("end", () => { req.close(); client.close(); endPromise.resolve(); }); req.end(); await endPromise.promise; assertEquals(client.socket.remoteAddress, "127.0.0.1"); assertEquals(client.socket.remotePort, 4246); assertEquals(client.socket.remoteFamily, "IPv4"); client.socket.setTimeout(0); assertEquals(receivedData, "hello world\n"); }); Deno.test("[node/http2 client] connection states", async () => { const expected = { beforeConnect: { connecting: true, closed: false, destroyed: false }, afterConnect: { connecting: false, closed: false, destroyed: false }, afterClose: { connecting: false, closed: true, destroyed: false }, afterDestroy: { connecting: false, closed: true, destroyed: true }, }; const actual: Partial = {}; const url = "http://127.0.0.1:4246"; const connectPromise = Promise.withResolvers(); const client = http2.connect(url, {}, () => { connectPromise.resolve(); }); client.on("error", (err) => console.error(err)); // close event happens after destory has been called const destroyPromise = Promise.withResolvers(); client.on("close", () => { destroyPromise.resolve(); }); actual.beforeConnect = { connecting: client.connecting, closed: client.closed, destroyed: client.destroyed, }; await connectPromise.promise; actual.afterConnect = { connecting: client.connecting, closed: client.closed, destroyed: client.destroyed, }; // leave a request open to prevent immediate destroy const req = client.request(); req.on("data", () => {}); req.on("error", (err) => console.error(err)); const reqClosePromise = Promise.withResolvers(); req.on("close", () => { reqClosePromise.resolve(); }); client.close(); actual.afterClose = { connecting: client.connecting, closed: client.closed, destroyed: client.destroyed, }; await destroyPromise.promise; actual.afterDestroy = { connecting: client.connecting, closed: client.closed, destroyed: client.destroyed, }; await reqClosePromise.promise; assertEquals(actual, expected); }); Deno.test("request and response exports", () => { assert(http2.Http2ServerRequest); assert(http2.Http2ServerResponse); });