mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
1303 lines
47 KiB
JavaScript
1303 lines
47 KiB
JavaScript
import http2 from "node:http2";
|
|
import { Duplex } from "stream";
|
|
import tls from "node:tls";
|
|
import net from "node:net";
|
|
import { which } from "bun";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { bunExe, bunEnv, expectMaxObjectTypeCount } from "harness";
|
|
import { tmpdir } from "node:os";
|
|
import http2utils from "./helpers";
|
|
import { afterAll, describe, beforeAll, it, test, expect } from "vitest";
|
|
import { TLS_OPTIONS, nodeEchoServer, TLS_CERT } from "./http2-helpers";
|
|
|
|
const nodeExecutable = which("node");
|
|
let nodeEchoServer_;
|
|
|
|
let HTTPS_SERVER;
|
|
beforeAll(async () => {
|
|
nodeEchoServer_ = await nodeEchoServer();
|
|
HTTPS_SERVER = nodeEchoServer_.url;
|
|
});
|
|
afterAll(async () => {
|
|
nodeEchoServer_.subprocess?.kill?.(9);
|
|
});
|
|
|
|
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();
|
|
${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",
|
|
});
|
|
subprocess.unref();
|
|
const reader = subprocess.stdout.getReader();
|
|
const data = await reader.read();
|
|
const decoder = new TextDecoder("utf-8");
|
|
const address = JSON.parse(decoder.decode(data.value));
|
|
const url = `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`;
|
|
return { address, url, subprocess };
|
|
}
|
|
|
|
function doHttp2Request(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);
|
|
|
|
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 () => {
|
|
const result = await doHttp2Request(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 () => {
|
|
const payload = JSON.stringify({ "hello": "bun" });
|
|
const result = await doHttp2Request(
|
|
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 () => {
|
|
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 () => {
|
|
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("should be able to mutiplex POST requests", async () => {
|
|
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: true,
|
|
initialWindowSize: 65535,
|
|
maxFrameSize: 16384,
|
|
maxConcurrentStreams: 2147483647,
|
|
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 () => {
|
|
try {
|
|
await doHttp2Request(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 9");
|
|
}
|
|
});
|
|
it("should be destroyed after close", async () => {
|
|
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.on("error", reject);
|
|
req.on("end", () => {
|
|
client.close();
|
|
});
|
|
req.end();
|
|
await promise;
|
|
expect(client.destroyed).toBe(true);
|
|
});
|
|
it("should be destroyed after destroy", async () => {
|
|
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.on("end", () => {
|
|
client.destroy();
|
|
});
|
|
req.end();
|
|
await promise;
|
|
expect(client.destroyed).toBe(true);
|
|
});
|
|
it("should fail to connect over HTTP/1.1", async () => {
|
|
const tls = TLS_CERT;
|
|
using server = Bun.serve({
|
|
port: 0,
|
|
hostname: "127.0.0.1",
|
|
tls: {
|
|
...tls,
|
|
ca: TLS_CERT.ca,
|
|
},
|
|
fetch() {
|
|
return new Response("hello");
|
|
},
|
|
});
|
|
const url = `https://127.0.0.1:${server.port}`;
|
|
try {
|
|
await doHttp2Request(url, { ":path": "/" }, null, TLS_OPTIONS);
|
|
expect("unreachable").toBe(true);
|
|
} catch (err) {
|
|
expect(err.code).toBe("ERR_HTTP2_ERROR");
|
|
}
|
|
});
|
|
it("works with Duplex", async () => {
|
|
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}/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 () => {
|
|
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 () => {
|
|
const abortController = new AbortController();
|
|
const promise = doHttp2Request(`${HTTPS_SERVER}/get`, { ":path": "/get" }, null, null, {
|
|
signal: abortController.signal,
|
|
});
|
|
abortController.abort();
|
|
try {
|
|
await promise;
|
|
expect("unreachable").toBe(true);
|
|
} catch (err) {
|
|
expect(err.errno).toBe(http2.constants.NGHTTP2_CANCEL);
|
|
}
|
|
});
|
|
it("aborted event should work with abortController", async () => {
|
|
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": "/" }, { signal: abortController.signal });
|
|
req.on("aborted", resolve);
|
|
req.on("error", err => {
|
|
if (err.errno !== http2.constants.NGHTTP2_CANCEL) {
|
|
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(8);
|
|
});
|
|
it("aborted event should work with aborted signal", async () => {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
|
|
client.on("error", reject);
|
|
const req = client.request({ ":path": "/" }, { signal: AbortSignal.abort() });
|
|
req.on("aborted", resolve);
|
|
req.on("error", err => {
|
|
if (err.errno !== http2.constants.NGHTTP2_CANCEL) {
|
|
reject(err);
|
|
}
|
|
});
|
|
req.on("end", () => {
|
|
reject();
|
|
client.close();
|
|
});
|
|
const result = await promise;
|
|
expect(result).toBeUndefined();
|
|
expect(req.rstCode).toBe(8);
|
|
expect(req.aborted).toBeTrue();
|
|
});
|
|
it("endAfterHeaders should work", async () => {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
|
|
client.on("error", reject);
|
|
const req = client.request({ ":path": "/" });
|
|
req.endAfterHeaders = true;
|
|
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", console.error);
|
|
req.on("end", () => {
|
|
resolve();
|
|
});
|
|
await promise;
|
|
expect(response_headers[":status"]).toBe(200);
|
|
expect(data).toBeFalsy();
|
|
});
|
|
it("state should work", async () => {
|
|
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.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(0);
|
|
expect(client.pendingSettingsAck).toBeTrue();
|
|
let received_origin = null;
|
|
client.on("origin", origin => {
|
|
received_origin = origin;
|
|
});
|
|
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(headers);
|
|
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.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.originSet.length).toBe(1);
|
|
expect(client.originSet).toEqual(received_origin);
|
|
expect(client.originSet[0]).toBe("www.example.com");
|
|
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 () => {
|
|
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 () => {
|
|
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 () => {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
|
|
client.on("error", resolve);
|
|
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 () => {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS);
|
|
client.on("error", resolve);
|
|
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 () => {
|
|
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("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);
|
|
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("should not leak memory", () => {
|
|
const { stdout, exitCode } = Bun.spawnSync({
|
|
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(nodeEchoServer_),
|
|
HTTP2_SERVER_TLS: JSON.stringify(TLS_OPTIONS),
|
|
},
|
|
stderr: "inherit",
|
|
stdin: "inherit",
|
|
stdout: "inherit",
|
|
});
|
|
expect(exitCode).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(0);
|
|
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(0);
|
|
expect(opaqueData.toString()).toBe("");
|
|
} finally {
|
|
server.subprocess.kill();
|
|
}
|
|
});
|
|
it("should not be able to write on socket", done => {
|
|
const client = http2.connect(HTTPS_SERVER, TLS_OPTIONS, (session, socket) => {
|
|
try {
|
|
client.socket.write("hello");
|
|
client.socket.end();
|
|
expect().fail("unreachable");
|
|
} catch (err) {
|
|
try {
|
|
expect(err.code).toBe("ERR_HTTP2_NO_SOCKET_MANIPULATION");
|
|
} catch (err) {
|
|
done(err);
|
|
}
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
it("should handle bad GOAWAY server frame size", done => {
|
|
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", async () => {
|
|
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 6");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
it("should handle bad DATA_FRAME server frame size", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 6");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
it("should handle bad RST_FRAME server frame size (no stream)", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 1");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
it("should handle bad RST_FRAME server frame size (less than allowed)", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 6");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
it("should handle bad RST_FRAME server frame size (more than allowed)", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 6");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
it("should handle bad CONTINUATION_FRAME server frame size", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 1");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
it("should handle bad PRIOTITY_FRAME server frame size", done => {
|
|
const { promise: waitToWrite, resolve: allowWrite } = 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", async () => {
|
|
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 6");
|
|
done();
|
|
} catch (err) {
|
|
done(err);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
});
|
|
});
|