Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/js/internal/test/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,64 @@ class TestTCPWrap {
}
}

// Transcription of nghttp2's `nghttp2_strerror`, which is what Node's binding
// returns here. Bun's engine emits only a few of these codes; those carry the
// same messages (kNghttp2ErrorMessages in node/http2.ts).
const nghttp2ErrorMessages: Record<number, string> = {
[0]: "Success",
[-501]: "Invalid argument",
[-502]: "Out of buffer space",
[-503]: "Unsupported SPDY version",
[-504]: "Operation would block",
[-505]: "Protocol error",
[-506]: "Invalid frame octets",
[-507]: "EOF",
[-508]: "Data transfer deferred",
[-509]: "No more Stream ID available",
[-510]: "Stream was already closed or invalid",
[-511]: "Stream is closing",
[-512]: "The transmission is not allowed for this stream",
[-513]: "Stream ID is invalid",
[-514]: "Invalid stream state",
[-515]: "Another DATA frame has already been deferred",
[-516]: "request HEADERS is not allowed",
[-517]: "GOAWAY has already been sent",
[-518]: "Invalid header block",
[-519]: "Invalid state",
[-521]: "The user callback function failed due to the temporal error",
[-522]: "The length of the frame is invalid",
[-523]: "Header compression/decompression error",
[-524]: "Flow control error",
[-525]: "Insufficient buffer size given to function",
[-526]: "Callback was paused by the application",
[-527]: "Too many inflight SETTINGS",
[-528]: "Server push is disabled by peer",
[-529]: "DATA or HEADERS frame has already been submitted for the stream",
[-530]: "The current session is closing",
[-531]: "Invalid HTTP header field was received",
[-532]: "Violation in HTTP messaging rule",
[-533]: "Stream was refused",
[-534]: "Internal error",
[-535]: "Cancel",
[-536]: "When a local endpoint expects to receive SETTINGS frame, it receives an other type of frame",
[-537]: "SETTINGS frame contained more than the maximum allowed entries",
[-901]: "Out of memory",
[-902]: "The user callback function failed",
[-903]: "Received bad client magic byte string",
[-904]: "Flooding was detected in this HTTP/2 session, and it must be closed",
[-905]: "Too many CONTINUATION frames following a HEADER frame",
};

function nghttp2ErrorString(code: number) {
return nghttp2ErrorMessages[code] || "Unknown error code";
}

// Node's `internalBinding('http2')` exposes the native handle classes beneath
// Http2Session/Http2Stream; Bun drives the Rust engine directly and has no such
// layer. These stand-ins give tests that stub a handle method something inert.
class Http2Session {}
class Http2Stream {}

function internalBinding(name: string) {
switch (name) {
case "trace_events":
Expand All @@ -63,6 +121,15 @@ function internalBinding(name: string) {
// process.binding("uv") carries libuv's own codes on every platform
// (including Windows' synthetic ones), same as node's uv binding.
return process.binding("uv");
// The constants table is the very same one node:http2 exposes publicly, so
// the vendored tests check Bun's real values.
case "http2":
return {
Http2Session,
Http2Stream,
constants: require("node:http2").constants,
nghttp2ErrorString,
};
case "udp_wrap":
return { UDP: require("internal/dgram").UDP };
case "tcp_wrap":
Expand Down
262 changes: 262 additions & 0 deletions test/js/node/test/parallel/test-http2-binding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Flags: --expose-internals
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { internalBinding } = require('internal/test/binding');

const binding = internalBinding('http2');
const http2 = require('http2');

assert(binding.Http2Session);
assert.strictEqual(typeof binding.Http2Session, 'function');

const settings = http2.getDefaultSettings();
assert.strictEqual(settings.headerTableSize, 4096);
assert.strictEqual(settings.enablePush, true);
assert.strictEqual(settings.maxConcurrentStreams, 4294967295);
assert.strictEqual(settings.initialWindowSize, 65535);
assert.strictEqual(settings.maxFrameSize, 16384);

assert.strictEqual(binding.nghttp2ErrorString(-517),
'GOAWAY has already been sent');

// Assert constants are present
assert(binding.constants);
assert.strictEqual(typeof binding.constants, 'object');
const constants = binding.constants;

const expectedStatusCodes = {
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
};

const expectedHeaderNames = {
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_DATE: 'date',
HTTP2_HEADER_ACCEPT_CHARSET: 'accept-charset',
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_MAX_AGE: 'access-control-max-age',
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_ALLOW: 'allow',
HTTP2_HEADER_AUTHORIZATION: 'authorization',
HTTP2_HEADER_CACHE_CONTROL: 'cache-control',
HTTP2_HEADER_CONTENT_DISPOSITION: 'content-disposition',
HTTP2_HEADER_CONTENT_ENCODING: 'content-encoding',
HTTP2_HEADER_CONTENT_LANGUAGE: 'content-language',
HTTP2_HEADER_CONTENT_LENGTH: 'content-length',
HTTP2_HEADER_CONTENT_LOCATION: 'content-location',
HTTP2_HEADER_CONTENT_RANGE: 'content-range',
HTTP2_HEADER_CONTENT_TYPE: 'content-type',
HTTP2_HEADER_COOKIE: 'cookie',
HTTP2_HEADER_CONNECTION: 'connection',
HTTP2_HEADER_DNT: 'dnt',
HTTP2_HEADER_ETAG: 'etag',
HTTP2_HEADER_EXPECT: 'expect',
HTTP2_HEADER_EXPIRES: 'expires',
HTTP2_HEADER_FORWARDED: 'forwarded',
HTTP2_HEADER_FROM: 'from',
HTTP2_HEADER_HOST: 'host',
HTTP2_HEADER_IF_MATCH: 'if-match',
HTTP2_HEADER_IF_MODIFIED_SINCE: 'if-modified-since',
HTTP2_HEADER_IF_NONE_MATCH: 'if-none-match',
HTTP2_HEADER_IF_RANGE: 'if-range',
HTTP2_HEADER_IF_UNMODIFIED_SINCE: 'if-unmodified-since',
HTTP2_HEADER_LAST_MODIFIED: 'last-modified',
HTTP2_HEADER_LINK: 'link',
HTTP2_HEADER_LOCATION: 'location',
HTTP2_HEADER_MAX_FORWARDS: 'max-forwards',
HTTP2_HEADER_PREFER: 'prefer',
HTTP2_HEADER_PROXY_AUTHENTICATE: 'proxy-authenticate',
HTTP2_HEADER_PROXY_AUTHORIZATION: 'proxy-authorization',
HTTP2_HEADER_PROXY_CONNECTION: 'proxy-connection',
HTTP2_HEADER_RANGE: 'range',
HTTP2_HEADER_REFERER: 'referer',
HTTP2_HEADER_REFRESH: 'refresh',
HTTP2_HEADER_RETRY_AFTER: 'retry-after',
HTTP2_HEADER_SERVER: 'server',
HTTP2_HEADER_SET_COOKIE: 'set-cookie',
HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: 'strict-transport-security',
HTTP2_HEADER_TRAILER: 'trailer',
HTTP2_HEADER_TRANSFER_ENCODING: 'transfer-encoding',
HTTP2_HEADER_TK: 'tk',
HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS: 'upgrade-insecure-requests',
HTTP2_HEADER_USER_AGENT: 'user-agent',
HTTP2_HEADER_VARY: 'vary',
HTTP2_HEADER_VIA: 'via',
HTTP2_HEADER_WARNING: 'warning',
HTTP2_HEADER_WWW_AUTHENTICATE: 'www-authenticate',
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_CONTENT_MD5: 'content-md5',
HTTP2_HEADER_TE: 'te',
HTTP2_HEADER_UPGRADE: 'upgrade',
HTTP2_HEADER_HTTP2_SETTINGS: 'http2-settings',
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',
};

const expectedNGConstants = {
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_HCAT_REQUEST: 0,
NGHTTP2_HCAT_RESPONSE: 1,
NGHTTP2_HCAT_PUSH_RESPONSE: 2,
NGHTTP2_HCAT_HEADERS: 3,
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_NV_FLAG_NONE: 0,
NGHTTP2_NV_FLAG_NO_INDEX: 1,
NGHTTP2_ERR_DEFERRED: -508,
NGHTTP2_ERR_NOMEM: -901,
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: -509,
NGHTTP2_ERR_INVALID_ARGUMENT: -501,
NGHTTP2_ERR_STREAM_CLOSED: -510,
NGHTTP2_ERR_FRAME_SIZE_ERROR: -522,
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,
NGHTTP2_DEFAULT_WEIGHT: 16,
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
};

const defaultSettings = {
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
};

for (const name of Object.keys(constants)) {
if (name.startsWith('HTTP_STATUS_')) {
assert.strictEqual(constants[name], expectedStatusCodes[name],
`Expected status code match for ${name}`);
} else if (name.startsWith('HTTP2_HEADER_')) {
assert.strictEqual(constants[name], expectedHeaderNames[name],
`Expected header name match for ${name}`);
} else if (name.startsWith('NGHTTP2_')) {
assert.strictEqual(constants[name], expectedNGConstants[name],
`Expected ng constant match for ${name}`);
} else if (name.startsWith('DEFAULT_SETTINGS_')) {
assert.strictEqual(constants[name], defaultSettings[name],
`Expected default setting match for ${name}`);
}
}
52 changes: 52 additions & 0 deletions test/js/node/test/parallel/test-http2-respond-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';
// Flags: --expose-internals

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const { internalBinding } = require('internal/test/binding');
const { Http2Stream } = internalBinding('http2');

const server = http2.createServer();

Http2Stream.prototype.respond = () => 1;
server.on('stream', common.mustCall((stream) => {

// Send headers
stream.respond({ 'content-type': 'text/plain' });

// Should throw if headers already sent
assert.throws(
() => stream.respond(),
{
name: 'Error',
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);

// Should throw if stream already destroyed
stream.destroy();
assert.throws(
() => stream.respond(),
{
name: 'Error',
code: 'ERR_HTTP2_INVALID_STREAM',
message: 'The stream has been destroyed'
}
);
}));

server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();

req.on('end', common.mustCall(() => {
client.close();
server.close();
}));
req.resume();
req.end();
}));