compat(http2) more http2 compatibility improvements (#18060)

Co-authored-by: cirospaciari <6379399+cirospaciari@users.noreply.github.com>
This commit is contained in:
Ciro Spaciari
2025-03-11 19:46:05 -07:00
committed by GitHub
parent 7091fd5791
commit 4c93b72906
44 changed files with 2740 additions and 87 deletions

View File

@@ -90,6 +90,7 @@ const ErrorCode = enum(u32) {
ENHANCE_YOUR_CALM = 0xb,
INADEQUATE_SECURITY = 0xc,
HTTP_1_1_REQUIRED = 0xd,
MAX_PENDING_SETTINGS_ACK = 0xe,
_, // we can have unsupported extension/custom error codes types
};
@@ -685,6 +686,8 @@ pub const H2FrameParser = struct {
usedWindowSize: u32 = 0,
maxHeaderListPairs: u32 = 128,
maxRejectedStreams: u32 = 100,
maxOutstandingSettings: u32 = 10,
outstandingSettings: u32 = 0,
rejectedStreams: u32 = 0,
maxSessionMemory: u32 = 10, //this limit is in MB
queuedDataSize: u64 = 0, // this is in bytes
@@ -717,7 +720,7 @@ pub const H2FrameParser = struct {
}
pub fn next(this: *StreamResumableIterator) ?*Stream {
var it = this.parser.streams.iterator();
if (it.index > it.hm.capacity()) return null;
if (it.index > it.hm.capacity() or this.index > it.hm.capacity()) return null;
// resume the iterator from the same index if possible
it.index = this.index;
while (it.next()) |item| {
@@ -1168,9 +1171,14 @@ pub const H2FrameParser = struct {
return true;
}
pub fn setSettings(this: *H2FrameParser, settings: FullSettingsPayload) void {
pub fn setSettings(this: *H2FrameParser, settings: FullSettingsPayload) bool {
log("HTTP_FRAME_SETTINGS ack false", .{});
if (this.outstandingSettings >= this.maxOutstandingSettings) {
this.sendGoAway(0, .MAX_PENDING_SETTINGS_ACK, "Maximum number of pending settings acknowledgements", this.lastStreamID, true);
return false;
}
var buffer: [FrameHeader.byteSize + FullSettingsPayload.byteSize]u8 = undefined;
@memset(&buffer, 0);
var stream = std.io.fixedBufferStream(&buffer);
@@ -1182,10 +1190,14 @@ pub const H2FrameParser = struct {
.length = 36,
};
_ = settingsHeader.write(@TypeOf(writer), writer);
this.outstandingSettings += 1;
this.localSettings = settings;
_ = this.localSettings.write(@TypeOf(writer), writer);
_ = this.write(&buffer);
_ = this.ajustWindowSize(null, @intCast(buffer.len));
return true;
}
pub fn abortStream(this: *H2FrameParser, stream: *Stream, abortReason: JSC.JSValue) void {
@@ -1344,6 +1356,7 @@ pub const H2FrameParser = struct {
.streamIdentifier = 0,
.length = 36,
};
this.outstandingSettings += 1;
_ = settingsHeader.write(@TypeOf(writer), writer);
_ = this.localSettings.write(@TypeOf(writer), writer);
_ = this.write(&preface_buffer);
@@ -2255,6 +2268,9 @@ pub const H2FrameParser = struct {
// we can now write any request
this.remoteSettings = this.localSettings;
if (this.outstandingSettings > 0) {
this.outstandingSettings -= 1;
}
this.dispatch(.onLocalSettings, this.localSettings.toJS(this.handlers.globalObject));
}
@@ -2557,7 +2573,27 @@ pub const H2FrameParser = struct {
const options = args_list.ptr[0];
try this.loadSettingsFromJSValue(globalObject, options);
this.setSettings(this.localSettings);
return JSValue.jsBoolean(this.setSettings(this.localSettings));
}
pub fn setLocalWindowSize(this: *H2FrameParser, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
JSC.markBinding(@src());
const args_list = callframe.arguments_old(1);
if (args_list.len < 1) {
return globalObject.throwInvalidArguments("Expected windowSize argument", .{});
}
const windowSize = args_list.ptr[0];
if (!windowSize.isNumber()) {
return globalObject.throwInvalidArguments("Expected windowSize to be a number", .{});
}
const windowSizeValue: u32 = windowSize.to(u32);
if (windowSizeValue > MAX_WINDOW_SIZE or windowSizeValue < 0) {
return globalObject.throw("Expected windowSize to be a number between 0 and 2^32-1", .{});
}
if (windowSizeValue > this.windowSize) {
this.windowSize = windowSizeValue;
}
return .undefined;
}
@@ -2571,7 +2607,7 @@ pub const H2FrameParser = struct {
const settings = this.remoteSettings orelse this.localSettings;
result.put(globalObject, JSC.ZigString.static("remoteWindowSize"), JSC.JSValue.jsNumber(settings.initialWindowSize));
result.put(globalObject, JSC.ZigString.static("localWindowSize"), JSC.JSValue.jsNumber(this.localSettings.initialWindowSize));
result.put(globalObject, JSC.ZigString.static("localWindowSize"), JSC.JSValue.jsNumber(this.windowSize));
result.put(globalObject, JSC.ZigString.static("deflateDynamicTableSize"), JSC.JSValue.jsNumber(settings.headerTableSize));
result.put(globalObject, JSC.ZigString.static("inflateDynamicTableSize"), JSC.JSValue.jsNumber(settings.headerTableSize));
result.put(globalObject, JSC.ZigString.static("outboundQueueSize"), JSC.JSValue.jsNumber(this.outboundQueueSize));
@@ -3784,7 +3820,7 @@ pub const H2FrameParser = struct {
if (end_stream_js.asBoolean()) {
end_stream = true;
// will end the stream after trailers
if (!waitForTrailers) {
if (!waitForTrailers or this.isServer) {
flags |= @intFromEnum(HeadersFrameFlags.END_STREAM);
}
}
@@ -4120,6 +4156,11 @@ pub const H2FrameParser = struct {
this.maxRejectedStreams = @truncate(max_rejected_streams.to(u64));
}
}
if (try settings_js.get(globalObject, "maxOutstandingSettings")) |max_outstanding_settings| {
if (max_outstanding_settings.isNumber()) {
this.maxOutstandingSettings = @max(1, @as(u32, @truncate(max_outstanding_settings.to(u64))));
}
}
}
}
var is_server = false;
@@ -4133,7 +4174,7 @@ pub const H2FrameParser = struct {
this.hpack = lshpack.HPACK.init(this.localSettings.headerTableSize);
if (is_server) {
this.setSettings(this.localSettings);
_ = this.setSettings(this.localSettings);
} else {
// consider that we need to queue until the first flush
this.has_nonnative_backpressure = true;

View File

@@ -190,6 +190,7 @@ const Handlers = struct {
const listen_socket: *Listener = @fieldParentPtr("handlers", this);
// allow it to be GC'd once the last connection is closed and it's not listening anymore
if (listen_socket.listener == .none) {
listen_socket.poll_ref.unref(this.vm);
listen_socket.strong_self.deinit();
}
} else {
@@ -957,9 +958,10 @@ pub const Listener = struct {
const listener = this.listener;
this.listener = .none;
this.poll_ref.unref(this.handlers.vm);
// if we already have no active connections, we can deinit the context now
if (this.handlers.active_connections == 0) {
this.poll_ref.unref(this.handlers.vm);
this.handlers.unprotect();
// deiniting the context will also close the listener
if (this.socket_context) |ctx| {

View File

@@ -37,6 +37,10 @@ export default [
fn: "updateSettings",
length: 1,
},
setLocalWindowSize: {
fn: "setLocalWindowSize",
length: 1,
},
read: {
fn: "read",
length: 1,

View File

@@ -153,6 +153,8 @@ const errors: ErrorCodeMapping = [
["ERR_HTTP2_STATUS_101", Error],
["ERR_HTTP2_INVALID_INFO_STATUS", RangeError],
["ERR_HTTP2_HEADERS_AFTER_RESPOND", Error],
["ERR_HTTP2_PUSH_DISABLED", Error],
["ERR_HTTP2_MAX_PENDING_SETTINGS_ACK", Error],
// AsyncHooks
["ERR_ASYNC_TYPE", TypeError],
["ERR_INVALID_ASYNC_ID", RangeError],

View File

@@ -344,6 +344,8 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
const message = this._httpMessage;
const req = message?.req;
if (req && !req.complete) {
// at this point the socket is already destroyed, lets avoid UAF
req[kHandle] = undefined;
req.destroy(new ConnResetException("aborted"));
}
}
@@ -382,6 +384,13 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
this[kHandle] = undefined;
handle.onclose = this.#onCloseForDestroy.bind(this, callback);
handle.close();
// lets sync check and destroy the request if it's not complete
const message = this._httpMessage;
const req = message?.req;
if (req && !req.complete) {
// at this point the handle is not destroyed yet, lets destroy the request
req.destroy(new ConnResetException("aborted"));
}
}
_final(callback) {

View File

@@ -6,6 +6,8 @@ const { hideFromStack, throwNotImplemented } = require("internal/shared");
const tls = require("node:tls");
const net = require("node:net");
const fs = require("node:fs");
const { $data } = require("node:fs/promises");
const FileHandle = $data.FileHandle;
const bunTLSConnectOptions = Symbol.for("::buntlsconnectoptions::");
const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::");
const kInfoHeaders = Symbol("sent-info-headers");
@@ -291,7 +293,7 @@ class Http2ServerRequest extends Readable {
stream.on("error", onStreamError);
stream.on("aborted", onStreamAbortedRequest);
stream.on("close", onStreamCloseRequest);
stream.on("timeout", onStreamTimeout);
stream.on("timeout", onStreamTimeout.bind(this));
this.on("pause", onRequestPause);
this.on("resume", onRequestResume);
}
@@ -406,7 +408,7 @@ class Http2ServerResponse extends Stream {
stream.on("aborted", onStreamAbortedResponse);
stream.on("close", onStreamCloseResponse);
stream.on("wantTrailers", onStreamTrailersReady);
stream.on("timeout", onStreamTimeout);
stream.on("timeout", onStreamTimeout.bind(this));
}
// User land modules such as finalhandler just check truthiness of this
@@ -521,7 +523,7 @@ class Http2ServerResponse extends Stream {
removeHeader(name) {
validateString(name, "name");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
name = StringPrototypeToLowerCase.$call(StringPrototypeTrim.$call(name));
@@ -536,7 +538,7 @@ class Http2ServerResponse extends Stream {
setHeader(name, value) {
validateString(name, "name");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
this[kSetHeader](name, value);
}
@@ -558,7 +560,7 @@ class Http2ServerResponse extends Stream {
appendHeader(name, value) {
validateString(name, "name");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
this[kAppendHeader](name, value);
}
@@ -614,7 +616,7 @@ class Http2ServerResponse extends Stream {
const state = this[kState];
if (state.closed || this.stream.destroyed) return this;
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated");
if (this[kStream].headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
if (typeof statusMessage === "string") statusMessageWarn();
@@ -1506,10 +1508,16 @@ class Http2Session extends EventEmitter {
}
function streamErrorFromCode(code: number) {
if (code === 0xe) {
return $ERR_HTTP2_MAX_PENDING_SETTINGS_ACK("Maximum number of pending settings acknowledgements");
}
return $ERR_HTTP2_STREAM_ERROR(`Stream closed with error code ${nameForErrorCode[code] || code}`);
}
hideFromStack(streamErrorFromCode);
function sessionErrorFromCode(code: number) {
if (code === 0xe) {
return $ERR_HTTP2_MAX_PENDING_SETTINGS_ACK("Maximum number of pending settings acknowledgements");
}
return $ERR_HTTP2_SESSION_ERROR(`Session closed with error code ${nameForErrorCode[code] || code}`);
}
hideFromStack(sessionErrorFromCode);
@@ -1568,6 +1576,7 @@ class Http2Stream extends Duplex {
constructor(streamId, session, headers) {
super({
decodeStrings: false,
autoDestroy: false,
});
this.#id = streamId;
this[bunHTTP2Session] = session;
@@ -1610,15 +1619,6 @@ class Http2Stream extends Duplex {
return !!this[kHeadRequest];
}
static #rstStream() {
const session = this[bunHTTP2Session];
assertSession(session);
markStreamClosed(this);
session[bunHTTP2Native]?.rstStream(this.#id, this.rstCode);
this[bunHTTP2Session] = null;
}
sendTrailers(headers) {
const session = this[bunHTTP2Session];
@@ -1728,6 +1728,7 @@ class Http2Stream extends Duplex {
}
_destroy(err, callback) {
const { ending } = this._writableState;
this.push(null);
if (!ending) {
// If the writable side of the Http2Stream is still open, emit the
@@ -1738,7 +1739,7 @@ class Http2Stream extends Duplex {
}
// at this state destroyed will be true but we need to close the writable side
this._writableState.destroyed = false;
this.end();
this.end(); // why this is needed?
// we now restore the destroyed flag
this._writableState.destroyed = true;
}
@@ -1760,15 +1761,9 @@ class Http2Stream extends Duplex {
}
}
if (this.writableFinished) {
markStreamClosed(this);
session[bunHTTP2Native]?.rstStream(this.#id, rstCode);
this[bunHTTP2Session] = null;
} else {
this.once("finish", Http2Stream.#rstStream);
}
markStreamClosed(this);
session[bunHTTP2Native]?.rstStream(this.#id, rstCode);
this[bunHTTP2Session] = null;
callback(err);
}
@@ -1789,6 +1784,15 @@ class Http2Stream extends Duplex {
end(chunk, encoding, callback) {
const status = this[bunHTTP2StreamStatus];
if (typeof callback === "undefined") {
if (typeof chunk === "function") {
callback = chunk;
chunk = undefined;
} else if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
}
if ((status & StreamState.EndedCalled) !== 0) {
typeof callback == "function" && callback();
@@ -1872,10 +1876,15 @@ function tryClose(fd) {
function doSendFileFD(options, fd, headers, err, stat) {
const onError = options.onError;
if (err) {
tryClose(fd);
if (err.code !== "EBADF") {
tryClose(fd);
}
if (onError) onError(err);
else this.destroy(err);
else {
this.respond(headers, options);
this.destroy(streamErrorFromCode(NGHTTP2_INTERNAL_ERROR));
}
return;
}
@@ -1893,17 +1902,21 @@ function doSendFileFD(options, fd, headers, err, stat) {
: $ERR_HTTP2_SEND_FILE_NOSEEK("Offset or length can only be specified for regular files");
tryClose(fd);
if (onError) onError(err);
else this.destroy(err);
else {
this.respond(headers, options);
this.destroy(err);
}
return;
}
options.offset = -1;
options.offset = 0;
options.length = -1;
}
if (this.destroyed || this.closed) {
tryClose(fd);
const error = $ERR_HTTP2_INVALID_STREAM(`The stream has been destroyed`);
this.respond(headers, options);
this.destroy(error);
return;
}
@@ -1912,14 +1925,23 @@ function doSendFileFD(options, fd, headers, err, stat) {
offset: options.offset !== undefined ? options.offset : 0,
length: options.length !== undefined ? options.length : -1,
};
if (statOptions.offset <= 0) {
statOptions.offset = 0;
}
if (statOptions.length <= 0) {
if (stat.isFile()) {
statOptions.length = stat.size;
} else {
statOptions.length = undefined;
}
}
// options.statCheck is a user-provided function that can be used to
// verify stat values, override or set headers, or even cancel the
// response operation. If statCheck explicitly returns false, the
// response is canceled. The user code may also send a separate type
// of response so check again for the HEADERS_SENT flag
if (
(typeof options.statCheck === "function" && options.statCheck.$call(this, [stat, headers]) === false) ||
(typeof options.statCheck === "function" && options.statCheck.$call(this, stat, headers, options) === false) ||
this.headersSent
) {
tryClose(fd);
@@ -1938,9 +1960,9 @@ function doSendFileFD(options, fd, headers, err, stat) {
this.respond(headers, options);
fs.createReadStream(null, {
fd: fd,
autoClose: true,
start: statOptions.offset,
end: statOptions.length,
autoClose: false,
start: statOptions.offset ? statOptions.offset : undefined,
end: typeof statOptions.length === "number" ? statOptions.length + (statOptions.offset || 0) - 1 : undefined,
emitClose: false,
}).pipe(this);
} catch (err) {
@@ -1973,10 +1995,15 @@ class ServerHttp2Stream extends Http2Stream {
super(streamId, session, headers);
}
pushStream() {
throwNotImplemented("ServerHttp2Stream.prototype.pushStream()");
throw $ERR_HTTP2_PUSH_DISABLED("HTTP/2 client has disabled push streams");
}
respondWithFile(path, headers, options) {
if (this.destroyed || this.closed) {
throw $ERR_HTTP2_INVALID_STREAM(`The stream has been destroyed`);
}
if (this.headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
if (headers == undefined) {
headers = {};
} else if (!$isObject(headers)) {
@@ -1985,10 +2012,11 @@ class ServerHttp2Stream extends Http2Stream {
headers = { ...headers };
}
if (headers[":status"] === undefined) {
headers[":status"] = 200;
if (headers[HTTP2_HEADER_STATUS] === undefined) {
headers[HTTP2_HEADER_STATUS] = 200;
}
const statusCode = (headers[":status"] |= 0);
const statusCode = headers[HTTP2_HEADER_STATUS];
options = { ...options };
// Payload/DATA frames are not permitted in these cases
if (
@@ -2000,11 +2028,23 @@ class ServerHttp2Stream extends Http2Stream {
throw $ERR_HTTP2_PAYLOAD_FORBIDDEN(`Responses with ${statusCode} status must not have a payload`);
}
if (options.offset !== undefined && typeof options.offset !== "number") {
throw $ERR_INVALID_ARG_VALUE("options.offset", options.offset);
}
if (options.length !== undefined && typeof options.length !== "number") {
throw $ERR_INVALID_ARG_VALUE("options.length", options.length);
}
if (options.statCheck !== undefined && typeof options.statCheck !== "function") {
throw $ERR_INVALID_ARG_VALUE("options.statCheck", options.statCheck);
}
fs.open(path, "r", afterOpen.bind(this, options || {}, headers));
}
respondWithFD(fd, headers, options) {
// TODO: optimize this
let { statCheck, offset, length } = options || {};
if (this.destroyed || this.closed) {
throw $ERR_HTTP2_INVALID_STREAM(`The stream has been destroyed`);
}
if (this.headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
if (headers == undefined) {
headers = {};
} else if (!$isObject(headers)) {
@@ -2013,10 +2053,10 @@ class ServerHttp2Stream extends Http2Stream {
headers = { ...headers };
}
if (headers[":status"] === undefined) {
headers[":status"] = 200;
if (headers[HTTP2_HEADER_STATUS] === undefined) {
headers[HTTP2_HEADER_STATUS] = 200;
}
const statusCode = (headers[":status"] |= 0);
const statusCode = headers[HTTP2_HEADER_STATUS];
// Payload/DATA frames are not permitted in these cases
if (
@@ -2027,7 +2067,21 @@ class ServerHttp2Stream extends Http2Stream {
) {
throw $ERR_HTTP2_PAYLOAD_FORBIDDEN(`Responses with ${statusCode} status must not have a payload`);
}
fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers));
options = { ...options };
if (options.offset !== undefined && typeof options.offset !== "number") {
throw $ERR_INVALID_ARG_VALUE("options.offset", options.offset);
}
if (options.length !== undefined && typeof options.length !== "number") {
throw $ERR_INVALID_ARG_VALUE("options.length", options.length);
}
if (options.statCheck !== undefined && typeof options.statCheck !== "function") {
throw $ERR_INVALID_ARG_VALUE("options.statCheck", options.statCheck);
}
if (fd instanceof FileHandle) {
fs.fstat(fd.fd, doSendFileFD.bind(this, options, fd, headers));
} else {
fs.fstat(fd, doSendFileFD.bind(this, options, fd, headers));
}
}
additionalHeaders(headers) {
if (this.destroyed || this.closed) {
@@ -2049,7 +2103,7 @@ class ServerHttp2Stream extends Http2Stream {
}
for (const name in headers) {
if (name.startsWith(":") && name !== ":status") {
if (name.startsWith(":") && name !== HTTP2_HEADER_STATUS) {
throw $ERR_HTTP2_INVALID_PSEUDOHEADER(`"${name}" is an invalid pseudoheader or is used incorrectly`);
}
}
@@ -2066,11 +2120,11 @@ class ServerHttp2Stream extends Http2Stream {
}
}
let hasStatus = true;
if (headers[":status"] === undefined) {
headers[":status"] = 200;
if (headers[HTTP2_HEADER_STATUS] === undefined) {
headers[HTTP2_HEADER_STATUS] = 200;
hasStatus = false;
}
const statusCode = (headers[":status"] |= 0);
const statusCode = headers[HTTP2_HEADER_STATUS];
if (hasStatus) {
if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS)
throw $ERR_HTTP2_STATUS_101("HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2");
@@ -2105,7 +2159,7 @@ class ServerHttp2Stream extends Http2Stream {
const session = this[bunHTTP2Session];
assertSession(session);
if (this.headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated");
if (this.headersSent) throw $ERR_HTTP2_HEADERS_SENT("Response has already been initiated.");
if (this.sentTrailers) {
throw $ERR_HTTP2_TRAILERS_ALREADY_SENT(`Trailing headers have already been sent`);
}
@@ -2129,8 +2183,20 @@ class ServerHttp2Stream extends Http2Stream {
sensitiveNames[sensitives[i]] = true;
}
}
if (headers[":status"] === undefined) {
headers[":status"] = 200;
if (headers[HTTP2_HEADER_STATUS] === undefined) {
headers[HTTP2_HEADER_STATUS] = 200;
}
const statusCode = headers[HTTP2_HEADER_STATUS];
let endStream = !!options?.endStream;
if (
endStream ||
statusCode === HTTP_STATUS_NO_CONTENT ||
statusCode === HTTP_STATUS_RESET_CONTENT ||
statusCode === HTTP_STATUS_NOT_MODIFIED ||
this.headRequest === true
) {
options = { ...options, endStream: true };
endStream = true;
}
if (typeof options === "undefined") {
@@ -2146,6 +2212,9 @@ class ServerHttp2Stream extends Http2Stream {
}
this.headersSent = true;
this[bunHTTP2Headers] = headers;
if (endStream) {
this.end();
}
return;
}
@@ -2482,7 +2551,6 @@ class ServerHttp2Session extends Http2Session {
}
}
this.emit("timeout");
this.destroy();
}
#onDrain() {
@@ -2689,7 +2757,7 @@ class ServerHttp2Session extends Http2Session {
}
setLocalWindowSize(windowSize) {
return this.#parser?.setLocalWindowSize(windowSize);
return this.#parser?.setLocalWindowSize?.(windowSize);
}
settings(settings: Settings, callback) {
@@ -2707,8 +2775,9 @@ class ServerHttp2Session extends Http2Session {
// If specified, the callback function is registered as a handler for the 'close' event.
close(callback: Function) {
this.#closed = true;
if (typeof callback === "function") {
this.once("close", callback);
this.on("close", callback);
}
if (this.#connections === 0) {
this.destroy();
@@ -2735,7 +2804,6 @@ class ServerHttp2Session extends Http2Session {
if (error) {
this.emit("error", error);
}
this.emit("close");
}
}
@@ -2836,7 +2904,7 @@ class ClientHttp2Session extends Http2Session {
if (!self || typeof stream !== "object") return;
const headers = toHeaderObject(rawheaders, sensitiveHeadersValue || []);
const status = stream[bunHTTP2StreamStatus];
const header_status = headers[":status"];
const header_status = headers[HTTP2_HEADER_STATUS];
if (header_status === HTTP_STATUS_CONTINUE) {
stream.emit("continue");
}
@@ -3006,7 +3074,6 @@ class ClientHttp2Session extends Http2Session {
}
}
this.emit("timeout");
this.destroy();
}
#onDrain() {
const parser = this.#parser;
@@ -3095,11 +3162,10 @@ class ClientHttp2Session extends Http2Session {
}
setLocalWindowSize(windowSize) {
return this.#parser?.setLocalWindowSize(windowSize);
return this.#parser?.setLocalWindowSize?.(windowSize);
}
get socket() {
if (this.#socket_proxy) return this.#socket_proxy;
const socket = this[bunHTTP2Socket];
if (!socket) return null;
this.#socket_proxy = new Proxy(this, proxySocketHandler);
@@ -3140,7 +3206,7 @@ class ClientHttp2Session extends Http2Session {
function onConnect() {
this.#onConnect(arguments);
listener?.$apply(this, arguments);
listener?.$call(this, this);
}
// h2 with ALPNProtocols
@@ -3204,6 +3270,9 @@ class ClientHttp2Session extends Http2Session {
destroy(error?: Error, code?: number) {
const socket = this[bunHTTP2Socket];
if (this.#closed && !this.#connected && !this.#parser) {
return;
}
this.#closed = true;
this.#connected = false;
if (socket) {
@@ -3221,7 +3290,6 @@ class ClientHttp2Session extends Http2Session {
if (error) {
this.emit("error", error);
}
this.emit("close");
}
@@ -3259,7 +3327,9 @@ class ClientHttp2Session extends Http2Session {
let authority = headers[":authority"];
if (!authority) {
authority = url.host;
headers[":authority"] = authority;
if (!headers["host"]) {
headers[":authority"] = authority;
}
}
let method = headers[":method"];
if (!method) {
@@ -3383,8 +3453,10 @@ function connectionListener(socket: Socket) {
"listener for the `unknownProtocol` event.\n",
);
}
return;
}
// setup session
const session = new ServerHttp2Session(socket, options, this);
session.on("error", sessionOnError);
const timeout = this.timeout;

View File

@@ -1080,12 +1080,12 @@ Socket.prototype.setTimeout = function setTimeout(timeout, callback) {
timeout = getTimerDuration(timeout, "msecs");
// internally or timeouts are in seconds
// we use Math.ceil because 0 would disable the timeout and less than 1 second but greater than 1ms would be 1 second (the minimum)
this._handle?.timeout(Math.ceil(timeout / 1000));
this.timeout = timeout;
if (callback !== undefined) {
validateFunction(callback, "callback");
this.once("timeout", callback);
}
this._handle?.timeout(Math.ceil(timeout / 1000));
this.timeout = timeout;
return this;
};
@@ -1110,6 +1110,7 @@ Socket.prototype.destroySoon = function destroySoon() {
else this.once("finish", this.destroy);
};
//TODO: migrate to native
Socket.prototype._writev = function _writev(data, callback) {
const allBuffers = data.allBuffers;

View File

@@ -1,4 +1,4 @@
import { bunEnv, bunExe, nodeExe } from "harness";
import { bunEnv, bunExe, nodeExe, isCI } from "harness";
import fs from "node:fs";
import http2 from "node:http2";
import net from "node:net";
@@ -957,21 +957,25 @@ for (const nodeExecutable of [nodeExe(), bunExe()]) {
]);
});
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 || 0).toBe(0);
}, 100000);
it.skipIf(!isCI)(
"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 || 0).toBe(0);
},
100000,
);
it("should receive goaway", async () => {
const { promise, resolve, reject } = Promise.withResolvers();

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const h2 = require('http2');
const assert = require('assert');
const server = h2.createServer(common.mustCall(function(req, res) {
req.on('aborted', common.mustCall(function() {
assert.strictEqual(this.aborted, true);
assert.strictEqual(this.complete, true);
}));
assert.strictEqual(req.aborted, false);
assert.strictEqual(req.complete, false);
res.write('hello');
server.close();
}));
server.listen(0, common.mustCall(function() {
const url = `http://localhost:${server.address().port}`;
const client = h2.connect(url, common.mustCall(() => {
const request = client.request();
request.on('data', common.mustCall((chunk) => {
client.destroy();
}));
}));
}));

View File

@@ -0,0 +1,44 @@
'use strict';
// Verifies that uploading data from a client works
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const fs = require('fs');
const fixtures = require('../common/fixtures');
const loc = fixtures.path('person-large.jpg');
assert(fs.existsSync(loc));
fs.readFile(loc, common.mustSucceed((data) => {
const server = http2.createServer(common.mustCall((req, res) => {
setImmediate(() => {
res.writeHead(400);
res.end();
});
}));
server.on('close', common.mustCall());
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
client.on('close', common.mustCall());
const req = client.request({ ':method': 'POST' });
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 400);
}));
req.resume();
req.on('end', common.mustCall(() => {
server.close();
client.close();
}));
const str = fs.createReadStream(loc);
str.pipe(req);
}));
}));

View File

@@ -0,0 +1,58 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const testResBody = 'other stuff!\n';
// Checks the full 100-continue flow from client sending 'expect: 100-continue'
// through server receiving it, triggering 'checkContinue' custom handler,
// writing the rest of the request to finally the client receiving to.
const server = http2.createServer(
common.mustNotCall('Full request received before 100 Continue')
);
server.on('checkContinue', common.mustCall((req, res) => {
res.writeContinue();
res.writeHead(200, {});
res.end(testResBody);
// Should simply return false if already too late to write
assert.strictEqual(res.writeContinue(), false);
res.on('finish', common.mustCall(
() => process.nextTick(() => assert.strictEqual(res.writeContinue(), false))
));
}));
server.listen(0, common.mustCall(() => {
let body = '';
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({
':method': 'POST',
'expect': '100-continue'
});
let gotContinue = false;
req.on('continue', common.mustCall(() => {
gotContinue = true;
}));
req.on('response', common.mustCall((headers) => {
assert.strictEqual(gotContinue, true);
assert.strictEqual(headers[':status'], 200);
req.end();
}));
req.setEncoding('utf-8');
req.on('data', common.mustCall((chunk) => { body += chunk; }));
req.on('end', common.mustCall(() => {
assert.strictEqual(body, testResBody);
client.close();
server.close();
}));
}));

View File

@@ -0,0 +1,62 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Requests using host instead of :authority should be allowed
// and Http2ServerRequest.authority should fall back to host
// :authority should NOT be auto-filled if host is present
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
const expected = {
':path': '/foobar',
':method': 'GET',
':scheme': 'http',
'host': `127.0.0.1:${port}`
};
assert.strictEqual(request.authority, expected.host);
const headers = request.headers;
for (const [name, value] of Object.entries(expected)) {
assert.strictEqual(headers[name], value);
}
const rawHeaders = request.rawHeaders;
for (const [name, value] of Object.entries(expected)) {
const position = rawHeaders.indexOf(name);
assert.notStrictEqual(position, -1);
assert.strictEqual(rawHeaders[position + 1], value);
}
assert(!Object.hasOwn(headers, ':authority'));
assert(!Object.hasOwn(rawHeaders, ':authority'));
response.on('finish', common.mustCall(function() {
server.close();
}));
response.end();
}));
const url = `http://127.0.0.1:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/foobar',
':method': 'GET',
':scheme': 'http',
'host': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,41 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
const msecs = common.platformTimeout(1);
const server = http2.createServer();
server.on('request', (req, res) => {
const request = req.setTimeout(msecs, common.mustCall(() => {
res.end();
}));
assert.strictEqual(request, req);
req.on('timeout', common.mustCall());
res.on('finish', common.mustCall(() => {
req.setTimeout(msecs, common.mustNotCall());
process.nextTick(() => {
req.setTimeout(msecs, common.mustNotCall());
server.close();
});
}));
});
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://127.0.0.1:${port}`);
const req = client.request({
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
});
req.on('end', common.mustCall(() => {
client.close();
}));
req.resume();
req.end();
}));

View File

@@ -0,0 +1,73 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerRequest should have getter for trailers & rawTrailers
const expectedTrailers = {
'x-foo': 'xOxOxOx, OxOxOxO, xOxOxOx, OxOxOxO',
'x-foo-test': 'test, test'
};
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
let data = '';
request.setEncoding('utf8');
request.on('data', common.mustCallAtLeast((chunk) => data += chunk));
request.on('end', common.mustCall(() => {
const trailers = request.trailers;
for (const [name, value] of Object.entries(expectedTrailers)) {
assert.strictEqual(trailers[name], value);
}
assert.deepStrictEqual([
'x-foo',
'xOxOxOx',
'x-foo',
'OxOxOxO',
'x-foo',
'xOxOxOx',
'x-foo',
'OxOxOxO',
'x-foo-test',
'test, test',
], request.rawTrailers);
assert.strictEqual(data, 'test\ntest');
response.end();
}));
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/foobar',
':method': 'POST',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers, { waitForTrailers: true });
request.on('wantTrailers', () => {
request.sendTrailers({
'x-fOo': 'xOxOxOx',
'x-foO': 'OxOxOxO',
'X-fOo': 'xOxOxOx',
'X-foO': 'OxOxOxO',
'x-foo-test': 'test, test'
});
});
request.resume();
request.on('end', common.mustCall(function() {
server.close();
client.close();
}));
request.write('test\n');
request.end('test');
}));
}));

View File

@@ -0,0 +1,54 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
const net = require('net');
// Http2ServerRequest should expose convenience properties
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
const expected = {
version: '2.0',
httpVersionMajor: 2,
httpVersionMinor: 0
};
assert.strictEqual(request.httpVersion, expected.version);
assert.strictEqual(request.httpVersionMajor, expected.httpVersionMajor);
assert.strictEqual(request.httpVersionMinor, expected.httpVersionMinor);
assert.ok(request.socket instanceof net.Socket);
assert.ok(request.connection instanceof net.Socket);
assert.strictEqual(request.socket, request.connection);
response.on('finish', common.mustCall(function() {
process.nextTick(() => {
// assert.ok(request.socket);
server.close();
});
}));
response.end();
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/foobar',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,31 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const h2 = require('http2');
// Server request and response should receive close event
// if the connection was terminated before response.end
// could be called or flushed
const server = h2.createServer(common.mustCall((req, res) => {
res.writeHead(200);
res.write('a');
req.on('close', common.mustCall());
res.on('close', common.mustCall());
req.on('error', common.mustNotCall());
}));
server.listen(0);
server.on('listening', () => {
const url = `http://localhost:${server.address().port}`;
const client = h2.connect(url, common.mustCall(() => {
const request = client.request();
request.on('data', common.mustCall(function(chunk) {
client.destroy();
server.close();
}));
}));
});

View File

@@ -0,0 +1,82 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const Countdown = require('../common/countdown');
// Check that destroying the Http2ServerResponse stream produces
// the expected result.
const errors = [
'test-error',
Error('test'),
];
let nextError;
const server = http2.createServer(common.mustCall((req, res) => {
req.on('error', common.mustNotCall());
res.on('error', common.mustNotCall());
res.on('finish', common.mustCall(() => {
res.destroy(nextError);
process.nextTick(() => {
res.destroy(nextError);
});
}));
if (req.url !== '/') {
nextError = errors.shift();
}
res.destroy(nextError);
}, 3));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const countdown = new Countdown(3, () => {
server.close();
client.close();
});
{
const req = client.request();
req.on('response', common.mustNotCall());
req.on('error', common.mustNotCall());
req.on('end', common.mustCall());
req.on('close', common.mustCall(() => countdown.dec()));
req.resume();
}
{
const req = client.request({ ':path': '/error' });
req.on('response', common.mustNotCall());
req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
name: 'Error',
message: 'Stream closed with error code NGHTTP2_INTERNAL_ERROR'
}));
req.on('close', common.mustCall(() => countdown.dec()));
req.resume();
req.on('end', common.mustNotCall());
}
{
const req = client.request({ ':path': '/error' });
req.on('response', common.mustNotCall());
req.on('error', common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
name: 'Error',
message: 'Stream closed with error code NGHTTP2_INTERNAL_ERROR'
}));
req.on('close', common.mustCall(() => countdown.dec()));
req.resume();
req.on('end', common.mustNotCall());
}
}));

View File

@@ -0,0 +1,357 @@
'use strict';
const {
mustCall,
mustNotCall,
hasCrypto,
platformTimeout,
skip
} = require('../common');
if (!hasCrypto)
skip('missing crypto');
const { strictEqual } = require('assert');
const {
createServer,
connect,
constants: {
HTTP2_HEADER_STATUS,
HTTP_STATUS_OK
}
} = require('http2');
{
// Http2ServerResponse.end accepts chunk, encoding, cb as args
// It may be invoked repeatedly without throwing errors
// but callback will only be called once
const server = createServer(mustCall((request, response) => {
response.end('end', 'utf8', mustCall(() => {
response.end(mustCall());
process.nextTick(() => {
response.end(mustCall());
server.close();
});
}));
response.on('finish', mustCall(() => {
response.end(mustCall());
}));
response.end(mustCall());
}));
server.listen(0, mustCall(() => {
let data = '';
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.setEncoding('utf8');
request.on('data', (chunk) => (data += chunk));
request.on('end', mustCall(() => {
strictEqual(data, 'end');
client.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Http2ServerResponse.end should return self after end
const server = createServer(mustCall((request, response) => {
strictEqual(response, response.end());
strictEqual(response, response.end());
server.close();
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.setEncoding('utf8');
request.on('end', mustCall(() => {
client.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Http2ServerResponse.end can omit encoding arg, sets it to utf-8
const server = createServer(mustCall((request, response) => {
response.end('test\uD83D\uDE00', mustCall(() => {
server.close();
}));
}));
server.listen(0, mustCall(() => {
let data = '';
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.setEncoding('utf8');
request.on('data', (chunk) => (data += chunk));
request.on('end', mustCall(() => {
strictEqual(data, 'test\uD83D\uDE00');
client.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Http2ServerResponse.end can omit chunk & encoding args
const server = createServer(mustCall((request, response) => {
response.end(mustCall(() => {
server.close();
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('data', mustNotCall());
request.on('end', mustCall(() => client.close()));
request.end();
request.resume();
}));
}));
}
{
// Http2ServerResponse.end is necessary on HEAD requests in compat
// for http1 compatibility
const server = createServer(mustCall((request, response) => {
strictEqual(response.writableEnded, false);
strictEqual(response.finished, false);
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
strictEqual(response.finished, false);
response.end('data', mustCall());
strictEqual(response.writableEnded, true);
strictEqual(response.finished, true);
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // The end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// .end should trigger 'end' event on request if user did not attempt
// to read from the request
const server = createServer(mustCall((request, response) => {
request.on('end', mustCall());
response.end();
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to call .end with cb from stream 'close'
const server = createServer(mustCall((request, response) => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.stream.on('close', mustCall(() => {
response.end(mustCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // The end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to respond to HEAD request after timeout
const server = createServer(mustCall((request, response) => {
setTimeout(mustCall(() => {
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.end('data', mustCall());
}), platformTimeout(10));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // The end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Finish should only trigger after 'end' is called
const server = createServer(mustCall((request, response) => {
let finished = false;
response.writeHead(HTTP_STATUS_OK, { foo: 'bar' });
response.on('finish', mustCall(() => {
finished = false;
}));
response.end('data', mustCall(() => {
strictEqual(finished, false);
response.end('data', mustCall());
}));
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // The end of stream flag is set
strictEqual(headers.foo, 'bar');
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}
{
// Should be able to respond to HEAD with just .end
const server = createServer(mustCall((request, response) => {
response.end('data', mustCall());
response.end(mustCall());
}));
server.listen(0, mustCall(() => {
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const client = connect(url, mustCall(() => {
const headers = {
':path': '/',
':method': 'HEAD',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
};
const request = client.request(headers);
request.on('response', mustCall((headers, flags) => {
strictEqual(headers[HTTP2_HEADER_STATUS], HTTP_STATUS_OK);
strictEqual(flags, 5); // The end of stream flag is set
}));
request.on('data', mustNotCall());
request.on('end', mustCall(() => {
client.close();
server.close();
}));
request.end();
request.resume();
}));
}));
}

View File

@@ -0,0 +1,188 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerResponse should support checking and reading custom headers
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
const real = 'foo-bar';
const fake = 'bar-foo';
const denormalised = ` ${real.toUpperCase()}\n\t`;
const expectedValue = 'abc123';
response.setHeader(real, expectedValue);
assert.strictEqual(response.hasHeader(real), true);
assert.strictEqual(response.hasHeader(fake), false);
assert.strictEqual(response.hasHeader(denormalised), true);
assert.strictEqual(response.getHeader(real), expectedValue);
assert.strictEqual(response.getHeader(denormalised), expectedValue);
assert.strictEqual(response.getHeader(fake), undefined);
response.removeHeader(fake);
assert.strictEqual(response.hasHeader(fake), false);
response.setHeader(real, expectedValue);
assert.strictEqual(response.getHeader(real), expectedValue);
assert.strictEqual(response.hasHeader(real), true);
response.removeHeader(real);
assert.strictEqual(response.hasHeader(real), false);
response.setHeader(denormalised, expectedValue);
assert.strictEqual(response.getHeader(denormalised), expectedValue);
assert.strictEqual(response.hasHeader(denormalised), true);
assert.strictEqual(response.hasHeader(real), true);
response.appendHeader(real, expectedValue);
assert.deepStrictEqual(response.getHeader(real), [
expectedValue,
expectedValue,
]);
assert.strictEqual(response.hasHeader(real), true);
response.removeHeader(denormalised);
assert.strictEqual(response.hasHeader(denormalised), false);
assert.strictEqual(response.hasHeader(real), false);
['hasHeader', 'getHeader', 'removeHeader'].forEach((fnName) => {
assert.throws(
() => response[fnName](),
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "name" argument must be of type string. Received ' +
'undefined'
}
);
});
[
':status',
':method',
':path',
':authority',
':scheme',
].forEach((header) => assert.throws(
() => response.setHeader(header, 'foobar'),
{
code: 'ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED',
name: 'TypeError',
message: 'Cannot set HTTP/2 pseudo-headers'
})
);
assert.throws(() => {
response.setHeader(real, null);
}, {
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "null" for header "foo-bar"'
});
assert.throws(() => {
response.setHeader(real, undefined);
}, {
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "foo-bar"'
});
assert.throws(
() => response.setHeader(), // Header name undefined
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "name" argument must be of type string. Received ' +
'undefined'
}
);
assert.throws(
() => response.setHeader(''),
{
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
// message: 'Header name must be a valid HTTP token [""]'
}
);
response.setHeader(real, expectedValue);
const expectedHeaderNames = [real];
assert.deepStrictEqual(response.getHeaderNames(), expectedHeaderNames);
const expectedHeaders = { __proto__: null };
expectedHeaders[real] = expectedValue;
assert.deepStrictEqual(response.getHeaders(), expectedHeaders);
response.getHeaders()[fake] = fake;
assert.strictEqual(response.hasHeader(fake), false);
assert.strictEqual(Object.getPrototypeOf(response.getHeaders()), null);
assert.strictEqual(response.sendDate, true);
response.sendDate = false;
assert.strictEqual(response.sendDate, false);
response.sendDate = true;
assert.strictEqual(response.sendDate, true);
response.removeHeader('Date');
assert.strictEqual(response.sendDate, false);
response.on('finish', common.mustCall(function() {
assert.strictEqual(response.headersSent, true);
assert.throws(
() => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
assert.throws(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
process.nextTick(() => {
assert.throws(
() => response.setHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
assert.throws(
() => response.removeHeader(real, expectedValue),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
assert.strictEqual(response.headersSent, true);
server.close();
});
}));
response.end();
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const msecs = common.platformTimeout(1);
const server = http2.createServer();
server.on('request', (req, res) => {
res.setTimeout(msecs, common.mustCall(() => {
res.end();
}));
res.on('timeout', common.mustCall());
res.on('finish', common.mustCall(() => {
res.setTimeout(msecs, common.mustNotCall());
process.nextTick(() => {
res.setTimeout(msecs, common.mustNotCall());
server.close();
});
}));
});
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://127.0.0.1:${port}`);
const req = client.request({
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `127.0.0.1:${port}`
});
req.on('end', common.mustCall(() => {
client.close();
}));
req.resume();
req.end();
}));

View File

@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerResponse.statusMessage should warn
const unsupportedWarned = common.mustCall(1);
process.on('warning', ({ name, message }) => {
const expectedMessage =
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)';
if (name === 'UnsupportedWarning' && message === expectedMessage)
unsupportedWarned();
});
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
response.on('finish', common.mustCall(function() {
response.statusMessage = 'test';
response.statusMessage = 'test'; // only warn once
assert.strictEqual(response.statusMessage, ''); // no change
server.close();
}));
response.end();
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
}, 1));
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,49 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerResponse.statusMessage should warn
const unsupportedWarned = common.mustCall(1);
process.on('warning', ({ name, message }) => {
const expectedMessage =
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)';
if (name === 'UnsupportedWarning' && message === expectedMessage)
unsupportedWarned();
});
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
response.on('finish', common.mustCall(function() {
assert.strictEqual(response.statusMessage, '');
assert.strictEqual(response.statusMessage, ''); // only warn once
server.close();
}));
response.end();
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
}, 1));
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,53 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const h2 = require('http2');
// Http2ServerResponse.writeHead should accept an optional status message
const unsupportedWarned = common.mustCall(1);
process.on('warning', ({ name, message }) => {
const expectedMessage =
'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)';
if (name === 'UnsupportedWarning' && message === expectedMessage)
unsupportedWarned();
});
const server = h2.createServer();
server.listen(0, common.mustCall(function() {
const port = server.address().port;
server.once('request', common.mustCall(function(request, response) {
const statusCode = 200;
const statusMessage = 'OK';
const headers = { 'foo-bar': 'abc123' };
response.writeHead(statusCode, statusMessage, headers);
response.on('finish', common.mustCall(function() {
server.close();
}));
response.end();
}));
const url = `http://localhost:${port}`;
const client = h2.connect(url, common.mustCall(function() {
const headers = {
':path': '/',
':method': 'GET',
':scheme': 'http',
':authority': `localhost:${port}`
};
const request = client.request(headers);
request.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
assert.strictEqual(headers['foo-bar'], 'abc123');
}, 1));
request.on('end', common.mustCall(function() {
client.close();
}));
request.end();
request.resume();
}));
}));

View File

@@ -0,0 +1,74 @@
'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.listen(0, common.mustCall(() => {
const port = server.address().port;
server.once('request', common.mustCall((request, response) => {
response.addTrailers({
ABC: 123
});
response.setTrailer('ABCD', 123);
assert.throws(
() => response.addTrailers({ '': 'test' }),
{
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
}
);
assert.throws(
() => response.setTrailer('test', undefined),
{
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "test"'
}
);
assert.throws(
() => response.setTrailer('test', null),
{
code: 'ERR_HTTP2_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "null" for header "test"'
}
);
assert.throws(
() => response.setTrailer(), // Trailer name undefined
{
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "name" argument must be of type string. Received ' +
'undefined'
}
);
assert.throws(
() => response.setTrailer(''),
{
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
}
);
response.end('hello');
}));
const url = `http://localhost:${port}`;
const client = http2.connect(url, common.mustCall(() => {
const request = client.request();
request.on('trailers', common.mustCall((headers) => {
assert.strictEqual(headers.abc, '123');
assert.strictEqual(headers.abcd, '123');
}));
request.resume();
request.on('end', common.mustCall(() => {
client.close();
server.close();
}));
}));
}));

View File

@@ -0,0 +1,91 @@
'use strict';
const {
mustCall,
mustNotCall,
hasCrypto,
skip
} = require('../common');
if (!hasCrypto)
skip('missing crypto');
const { createServer, connect } = require('http2');
const assert = require('assert');
{
const server = createServer();
server.listen(0, mustCall(() => {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const request = client.request();
request.resume();
request.on('end', mustCall());
request.on('close', mustCall(() => {
client.close();
}));
}));
server.once('request', mustCall((request, response) => {
// response.write() returns true
assert(response.write('muahaha', 'utf8', mustCall()));
response.stream.close(0, mustCall(() => {
response.on('error', mustNotCall());
// response.write() without cb returns error
response.write('muahaha', mustCall((err) => {
assert.strictEqual(err.code, 'ERR_HTTP2_INVALID_STREAM');
// response.write() with cb returns falsy value
assert(!response.write('muahaha', mustCall()));
client.destroy();
server.close();
}));
}));
}));
}));
}
{
// Http2ServerResponse.write ERR_STREAM_WRITE_AFTER_END
const server = createServer();
server.listen(0, mustCall(() => {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
const request = client.request();
request.resume();
request.on('end', mustCall());
request.on('close', mustCall(() => {
client.close();
}));
}));
server.once('request', mustCall((request, response) => {
response.end();
response.write('asd', mustCall((err) => {
assert.strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END');
client.destroy();
server.close();
}));
}));
}));
}
{
const server = createServer();
server.listen(0, mustCall(() => {
const port = server.address().port;
const url = `http://localhost:${port}`;
const client = connect(url, mustCall(() => {
client.request();
}));
server.once('request', mustCall((request, response) => {
response.destroy();
assert.strictEqual(response.write('asd', mustNotCall()), false);
client.destroy();
server.close();
}));
}));
}

View File

@@ -0,0 +1,147 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('node:assert');
const http2 = require('node:http2');
const debug = require('node:util').debuglog('test');
const testResBody = 'response content';
{
// Happy flow - string argument
const server = http2.createServer();
server.on('request', common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: '</styles.css>; rel=preload; as=style'
});
debug('Server sending full response...');
res.end(testResBody);
}));
server.listen(0);
server.on('listening', common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
req.on('headers', common.mustCall((headers) => {
assert.notStrictEqual(headers, undefined);
assert.strictEqual(headers[':status'], 103);
assert.strictEqual(headers.link, '</styles.css>; rel=preload; as=style');
}));
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 200);
}));
let data = '';
req.on('data', common.mustCallAtLeast((d) => data += d));
req.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(data, testResBody);
client.close();
server.close();
}));
}));
}
{
// Happy flow - array argument
const server = http2.createServer();
server.on('request', common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: [
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
]
});
debug('Server sending full response...');
res.end(testResBody);
}));
server.listen(0);
server.on('listening', common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
req.on('headers', common.mustCall((headers) => {
assert.notStrictEqual(headers, undefined);
assert.strictEqual(headers[':status'], 103);
assert.strictEqual(
headers.link,
'</styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script'
);
}));
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 200);
}));
let data = '';
req.on('data', common.mustCallAtLeast((d) => data += d));
req.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(data, testResBody);
client.close();
server.close();
}));
}));
}
{
// Happy flow - empty array
const server = http2.createServer();
server.on('request', common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
link: []
});
debug('Server sending full response...');
res.end(testResBody);
}));
server.listen(0);
server.on('listening', common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
debug('Client sending request...');
req.on('headers', common.mustNotCall());
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 200);
}));
let data = '';
req.on('data', common.mustCallAtLeast((d) => data += d));
req.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(data, testResBody);
client.close();
server.close();
}));
}));
}

View File

@@ -0,0 +1,40 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const assert = require('assert');
const server = http2.createServer((req, res) => {
console.log(`Connect from: ${req.connection.remoteAddress}`);
assert.strictEqual(req.connection.remoteAddress, '127.0.0.1');
req.on('end', common.mustCall(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`You are from: ${req.connection.remoteAddress}`);
}));
req.resume();
});
server.listen(0, '127.0.0.1', common.mustCall(() => {
const options = { localAddress: '127.0.0.1', family: 4 };
const client = http2.connect(
'http://localhost:' + server.address().port,
options
);
const req = client.request({
':path': '/'
});
req.on('data', () => req.resume());
req.on('end', common.mustCall(function() {
client.close();
req.close();
server.close();
}));
req.end();
}));

View File

@@ -0,0 +1,78 @@
'use strict';
// Flags: --expose-gc
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
// Tests that write uses the correct encoding when writing
// using the helper function createWriteReq
const testString = 'a\u00A1\u0100\uD83D\uDE00';
const encodings = {
'buffer': 'utf8',
'ascii': 'ascii',
'latin1': 'latin1',
'binary': 'latin1',
'utf8': 'utf8',
'utf-8': 'utf8',
'ucs2': 'ucs2',
'ucs-2': 'ucs2',
'utf16le': 'ucs2',
// 'utf-16le': 'ucs2',
'UTF8': 'utf8' // Should fall through to Buffer.from
};
const testsToRun = Object.keys(encodings).length;
let testsFinished = 0;
const server = http2.createServer(common.mustCall((req, res) => {
const testEncoding = encodings[req.url.slice(1)];
req.on('data', common.mustCall((chunk) => assert.ok(
Buffer.from(testString, testEncoding).equals(chunk)
)));
req.on('end', () => res.end());
}, Object.keys(encodings).length));
server.listen(0, common.mustCall(function() {
Object.keys(encodings).forEach((writeEncoding) => {
const client = http2.connect(`http://127.0.0.1:${this.address().port}`);
const req = client.request({
':path': `/${writeEncoding}`,
':method': 'POST'
});
assert.strictEqual(req._writableState.decodeStrings, false);
req.write(
writeEncoding !== 'buffer' ? testString : Buffer.from(testString),
writeEncoding !== 'buffer' ? writeEncoding : undefined
);
req.resume();
req.on('end', common.mustCall(function() {
client.close();
testsFinished++;
if (testsFinished === testsToRun) {
server.close(common.mustCall());
}
}));
// Ref: https://github.com/nodejs/node/issues/17840
const origDestroy = req.destroy;
req.destroy = function(...args) {
// Schedule a garbage collection event at the end of the current
// MakeCallback() run.
process.nextTick(globalThis.gc);
return origDestroy.call(this, ...args);
};
req.end();
});
}));

View File

@@ -0,0 +1,37 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const http2 = require('http2');
const assert = require('assert');
const server = http2.createServer();
server.on('session', common.mustCall(function(session) {
session.on('stream', common.mustCall(function(stream) {
stream.on('end', common.mustCall(function() {
this.respond({
':status': 200
});
this.write('foo');
this.destroy();
}));
stream.resume();
}));
}));
server.listen(0, function() {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const stream = client.request({ ':method': 'POST' });
stream.on('response', common.mustCall(function(headers) {
assert.strictEqual(headers[':status'], 200);
}));
stream.on('close', common.mustCall(() => {
client.close();
server.close();
}));
stream.resume();
stream.end();
});

View File

@@ -0,0 +1,40 @@
'use strict';
// Test sending a large stream with a large initial window size.
// See: https://github.com/nodejs/node/issues/19141
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const server = http2.createServer({ settings: { initialWindowSize: 6553500 } });
server.on('stream', (stream) => {
stream.resume();
stream.respond();
stream.end('ok');
});
server.listen(0, common.mustCall(() => {
let remaining = 1e8;
const chunkLength = 1e6;
const chunk = Buffer.alloc(chunkLength, 'a');
const client = http2.connect(`http://127.0.0.1:${server.address().port}`,
{ settings: { initialWindowSize: 6553500 } });
const request = client.request({ ':method': 'POST' });
function writeChunk() {
if (remaining > 0) {
remaining -= chunkLength;
request.write(chunk, writeChunk);
} else {
request.end();
}
}
writeChunk();
request.on('close', common.mustCall(() => {
client.close();
server.close();
}));
request.resume();
}));

View File

@@ -0,0 +1,47 @@
'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 server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
// Send headers
stream.respond({ 'content-type': 'text/plain' });
// Should throw if headers already sent
assert.throws(
() => stream.respond(),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
// Should throw if stream already destroyed
stream.destroy();
assert.throws(
() => stream.respond(),
{
code: 'ERR_HTTP2_INVALID_STREAM',
message: 'The stream has been destroyed'
}
);
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const req = client.request();
req.on('end', common.mustCall(() => {
client.close();
server.close();
}));
req.resume();
req.end();
}));

View File

@@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const http2 = require('http2');
const assert = require('assert');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_STATUS
} = http2.constants;
const fname = fixtures.path('elipses.txt');
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFile(fname, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
}, {
statCheck(stat, headers) {
// Abort the send and return a 304 Not Modified instead
stream.respond({ [HTTP2_HEADER_STATUS]: 304 });
return false;
}
});
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_STATUS], 304);
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], undefined);
}));
req.on('data', common.mustNotCall());
req.on('end', common.mustCall(() => {
client.close();
server.close();
}));
req.end();
});

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const assert = require('assert');
const path = require('path');
const {
HTTP2_HEADER_CONTENT_TYPE
} = http2.constants;
const server = http2.createServer();
server.on('stream', (stream) => {
const file = path.join(process.cwd(), 'not-a-file');
stream.respondWithFile(file, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
}, {
onError(err) {
common.expectsError({
code: 'ENOENT',
name: 'Error',
message: `ENOENT: no such file or directory, open '${file}'`
})(err);
stream.respond({ ':status': 404 });
stream.end();
},
statCheck: common.mustNotCall()
});
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 404);
}));
req.on('data', common.mustNotCall());
req.on('end', common.mustCall(() => {
client.close();
server.close();
}));
req.end();
});

View File

@@ -0,0 +1,102 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const http2 = require('http2');
const { inspect } = require('util');
const optionsWithTypeError = {
offset: 'number',
length: 'number',
statCheck: 'function'
};
const types = {
boolean: true,
function: () => {},
number: 1,
object: {},
array: [],
null: null,
symbol: Symbol('test')
};
const fname = fixtures.path('elipses.txt');
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
// Check for all possible TypeError triggers on options
Object.keys(optionsWithTypeError).forEach((option) => {
Object.keys(types).forEach((type) => {
if (type === optionsWithTypeError[option]) {
return;
}
assert.throws(
() => stream.respondWithFile(fname, {
'content-type': 'text/plain'
}, {
[option]: types[type]
}),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: `The property 'options.${option}' is invalid. ` +
`Received ${inspect(types[type])}`
}
);
});
});
// Should throw if :status 204, 205 or 304
[204, 205, 304].forEach((status) => assert.throws(
() => stream.respondWithFile(fname, {
'content-type': 'text/plain',
':status': status,
}),
{
code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN',
message: `Responses with ${status} status must not have a payload`
}
));
// Should throw if headers already sent
stream.respond({ ':status': 200 });
assert.throws(
() => stream.respondWithFile(fname, {
'content-type': 'text/plain'
}),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
// Should throw if stream already destroyed
stream.destroy();
assert.throws(
() => stream.respondWithFile(fname, {
'content-type': 'text/plain'
}),
{
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('close', common.mustCall(() => {
client.close();
server.close();
}));
req.end();
}));

View File

@@ -0,0 +1,121 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const http2 = require('http2');
const fs = require('fs');
const { inspect } = require('util');
const optionsWithTypeError = {
offset: 'number',
length: 'number',
statCheck: 'function'
};
const types = {
boolean: true,
function: () => {},
number: 1,
object: {},
array: [],
null: null,
symbol: Symbol('test')
};
const fname = fixtures.path('elipses.txt');
const fd = fs.openSync(fname, 'r');
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
// Should throw if fd isn't a number
Object.keys(types).forEach((type) => {
if (type === 'number') {
return;
}
assert.throws(
() => stream.respondWithFD(types[type], {
'content-type': 'text/plain'
}),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
}
);
});
// Check for all possible TypeError triggers on options
Object.keys(optionsWithTypeError).forEach((option) => {
Object.keys(types).forEach((type) => {
if (type === optionsWithTypeError[option]) {
return;
}
assert.throws(
() => stream.respondWithFD(fd, {
'content-type': 'text/plain'
}, {
[option]: types[type]
}),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: `The property 'options.${option}' is invalid. ` +
`Received ${inspect(types[type])}`
}
);
});
});
// Should throw if :status 204, 205 or 304
[204, 205, 304].forEach((status) => assert.throws(
() => stream.respondWithFD(fd, {
'content-type': 'text/plain',
':status': status,
}),
{
code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN',
name: 'Error',
message: `Responses with ${status} status must not have a payload`
}
));
// Should throw if headers already sent
stream.respond();
assert.throws(
() => stream.respondWithFD(fd, {
'content-type': 'text/plain'
}),
{
code: 'ERR_HTTP2_HEADERS_SENT',
message: 'Response has already been initiated.'
}
);
// Should throw if stream already destroyed
stream.destroy();
assert.throws(
() => stream.respondWithFD(fd, {
'content-type': 'text/plain'
}),
{
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('close', common.mustCall(() => {
client.close();
server.close();
}));
req.end();
}));

View File

@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const fs = require('fs');
const http2 = require('http2');
const {
NGHTTP2_INTERNAL_ERROR
} = http2.constants;
const errorCheck = common.expectsError({
code: 'ERR_HTTP2_STREAM_ERROR',
name: 'Error',
message: 'Stream closed with error code NGHTTP2_INTERNAL_ERROR'
}, 2);
const server = http2.createServer();
server.on('stream', (stream) => {
let fd = 2;
// Get first known bad file descriptor.
try {
while (fs.fstatSync(++fd));
} catch {
// Do nothing; we now have an invalid fd
}
stream.respondWithFD(fd);
stream.on('error', errorCheck);
});
server.listen(0, () => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall());
req.on('error', errorCheck);
req.on('data', common.mustNotCall());
req.on('end', common.mustNotCall());
req.on('close', common.mustCall(() => {
assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
client.close();
server.close();
}));
req.end();
});

View File

@@ -0,0 +1,94 @@
'use strict';
// Tests the ability to minimally request a byte range with respondWithFD
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const http2 = require('http2');
const assert = require('assert');
const fs = require('fs');
const Countdown = require('../common/countdown');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH
} = http2.constants;
const fname = fixtures.path('printA.js');
const data = fs.readFileSync(fname);
const fd = fs.openSync(fname, 'r');
// Note: this is not anywhere close to a proper implementation of the range
// header.
function getOffsetLength(range) {
if (range === undefined)
return [0, -1];
const r = /bytes=(\d+)-(\d+)/.exec(range);
return [+r[1], +r[2] - +r[1]];
}
const server = http2.createServer();
server.on('stream', (stream, headers) => {
const [ offset, length ] = getOffsetLength(headers.range);
stream.respondWithFD(fd, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
}, {
statCheck: common.mustCall((stat, headers, options) => {
assert.strictEqual(options.length, length);
assert.strictEqual(options.offset, offset);
headers['content-length'] =
Math.min(options.length, stat.size - offset);
}),
offset: offset,
length: length
});
});
server.on('close', common.mustCall(() => fs.closeSync(fd)));
server.listen(0, () => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const countdown = new Countdown(2, () => {
client.close();
server.close();
});
{
const req = client.request({ range: 'bytes=8-11' });
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers['content-type'], 'text/plain');
assert.strictEqual(+headers['content-length'], 3);
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8', 8, 11));
}));
req.on('close', common.mustCall(() => countdown.dec()));
req.end();
}
{
const req = client.request({ range: 'bytes=8-28' });
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 9);
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8', 8, 28));
}));
req.on('close', common.mustCall(() => countdown.dec()));
req.end();
}
});

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const assert = require('assert');
const fs = require('fs');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH
} = http2.constants;
const fname = fixtures.path('elipses.txt');
const data = fs.readFileSync(fname);
const stat = fs.statSync(fname);
const fd = fs.openSync(fname, 'r');
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFD(fd, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
[HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
});
});
server.on('close', common.mustCall(() => fs.closeSync(fd)));
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8'));
client.close();
server.close();
}));
req.end();
});

View File

@@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const assert = require('assert');
const fs = require('fs');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH
} = http2.constants;
const fname = fixtures.path('elipses.txt');
const data = fs.readFileSync(fname);
const stat = fs.statSync(fname);
fs.promises.open(fname, 'r').then(common.mustCall((fileHandle) => {
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFD(fileHandle, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
[HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
});
});
server.on('close', common.mustCall(() => fileHandle.close()));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8'));
client.close();
server.close();
}));
req.end();
}));
}));

View File

@@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const http2 = require('http2');
const assert = require('assert');
const fs = require('fs');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH,
HTTP2_HEADER_LAST_MODIFIED
} = http2.constants;
const fname = fixtures.path('printA.js');
const data = fs.readFileSync(fname);
const stat = fs.statSync(fname);
const server = http2.createServer();
server.on('stream', (stream) => {
stream.respondWithFile(fname, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
}, {
statCheck: common.mustCall((stat, headers) => {
headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString();
}),
offset: 8,
length: 3
});
});
server.listen(0, () => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 3);
assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED],
stat.mtime.toUTCString());
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8', 8, 11));
client.close();
server.close();
}));
req.end();
});

View File

@@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const http2 = require('http2');
const assert = require('assert');
const fs = require('fs');
const {
HTTP2_HEADER_CONTENT_TYPE,
HTTP2_HEADER_CONTENT_LENGTH,
HTTP2_HEADER_LAST_MODIFIED
} = http2.constants;
const fname = fixtures.path('elipses.txt');
const data = fs.readFileSync(fname);
const stat = fs.statSync(fname);
const server = http2.createServer();
server.on('stream', common.mustCall((stream) => {
stream.respondWithFile(fname, {
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
}, {
statCheck(stat, headers) {
headers[HTTP2_HEADER_LAST_MODIFIED] = stat.mtime.toUTCString();
headers[HTTP2_HEADER_CONTENT_LENGTH] = stat.size;
}
});
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], data.length);
assert.strictEqual(headers[HTTP2_HEADER_LAST_MODIFIED],
stat.mtime.toUTCString());
}));
req.setEncoding('utf8');
let check = '';
req.on('data', (chunk) => check += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(check, data.toString('utf8'));
client.close();
server.close();
}));
req.end();
}));

View File

@@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const http2 = require('http2');
const assert = require('assert');
const server = http2.createServer();
// Check that stream ends immediately after respond on :status 204, 205 & 304
const status = [204, 205, 304];
server.on('stream', common.mustCall((stream) => {
stream.on('close', common.mustCall(() => {
assert.strictEqual(stream.destroyed, true);
}));
stream.respond({ ':status': status.shift() });
}, 3));
server.listen(0, common.mustCall(makeRequest));
function makeRequest() {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request();
req.resume();
req.on('end', common.mustCall(() => {
client.close();
if (!status.length) {
server.close();
} else {
makeRequest();
}
}));
req.end();
}

View File

@@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const h2 = require('http2');
const server = h2.createServer();
server.listen(0, "127.0.0.1", common.mustCall(() => {
const afterConnect = common.mustCall((session) => {
session.request({ ':method': 'POST' }).end(common.mustCall(() => {
session.destroy();
server.close();
}));
});
const port = server.address().port;
const host = "127.0.0.1";
h2.connect(`http://${host}:${port}`, afterConnect);
}));

View File

@@ -0,0 +1,37 @@
'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) => {
stream.respond();
stream.end('ok');
}));
server.on('session', common.mustCall((session) => {
const windowSize = 2 ** 20;
const defaultSetting = http2.getDefaultSettings();
session.setLocalWindowSize(windowSize);
assert.strictEqual(session.state.effectiveLocalWindowSize, windowSize);
assert.strictEqual(session.state.localWindowSize, windowSize);
assert.strictEqual(
session.state.remoteWindowSize,
defaultSetting.initialWindowSize
);
}));
server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
const req = client.request();
req.resume();
req.on('close', common.mustCall(() => {
client.close();
server.close();
}));
}));