mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 11:29:02 +00:00
compat(http2) more http2 compatibility improvements (#18060)
Co-authored-by: cirospaciari <6379399+cirospaciari@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -37,6 +37,10 @@ export default [
|
||||
fn: "updateSettings",
|
||||
length: 1,
|
||||
},
|
||||
setLocalWindowSize: {
|
||||
fn: "setLocalWindowSize",
|
||||
length: 1,
|
||||
},
|
||||
read: {
|
||||
fn: "read",
|
||||
length: 1,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
29
test/js/node/test/parallel/test-http2-compat-aborted.js
Normal file
29
test/js/node/test/parallel/test-http2-compat-aborted.js
Normal 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();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
@@ -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);
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
@@ -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');
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
40
test/js/node/test/parallel/test-http2-connect-options.js
Normal file
40
test/js/node/test/parallel/test-http2-connect-options.js
Normal 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();
|
||||
}));
|
||||
78
test/js/node/test/parallel/test-http2-createwritereq.js
Normal file
78
test/js/node/test/parallel/test-http2-createwritereq.js
Normal 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();
|
||||
});
|
||||
}));
|
||||
37
test/js/node/test/parallel/test-http2-destroy-after-write.js
Normal file
37
test/js/node/test/parallel/test-http2-destroy-after-write.js
Normal 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();
|
||||
});
|
||||
40
test/js/node/test/parallel/test-http2-large-file.js
Normal file
40
test/js/node/test/parallel/test-http2-large-file.js
Normal 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();
|
||||
}));
|
||||
47
test/js/node/test/parallel/test-http2-respond-errors.js
Normal file
47
test/js/node/test/parallel/test-http2-respond-errors.js
Normal 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();
|
||||
}));
|
||||
45
test/js/node/test/parallel/test-http2-respond-file-304.js
Normal file
45
test/js/node/test/parallel/test-http2-respond-file-304.js
Normal 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();
|
||||
});
|
||||
47
test/js/node/test/parallel/test-http2-respond-file-404.js
Normal file
47
test/js/node/test/parallel/test-http2-respond-file-404.js
Normal 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();
|
||||
});
|
||||
102
test/js/node/test/parallel/test-http2-respond-file-errors.js
Normal file
102
test/js/node/test/parallel/test-http2-respond-file-errors.js
Normal 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();
|
||||
}));
|
||||
121
test/js/node/test/parallel/test-http2-respond-file-fd-errors.js
Normal file
121
test/js/node/test/parallel/test-http2-respond-file-fd-errors.js
Normal 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();
|
||||
}));
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
});
|
||||
47
test/js/node/test/parallel/test-http2-respond-file-fd.js
Normal file
47
test/js/node/test/parallel/test-http2-respond-file-fd.js
Normal 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();
|
||||
});
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
52
test/js/node/test/parallel/test-http2-respond-file-range.js
Normal file
52
test/js/node/test/parallel/test-http2-respond-file-range.js
Normal 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();
|
||||
});
|
||||
52
test/js/node/test/parallel/test-http2-respond-file.js
Normal file
52
test/js/node/test/parallel/test-http2-respond-file.js
Normal 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();
|
||||
}));
|
||||
39
test/js/node/test/parallel/test-http2-respond-no-data.js
Normal file
39
test/js/node/test/parallel/test-http2-respond-no-data.js
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
}));
|
||||
Reference in New Issue
Block a user