nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
1,607 lines (1,446 loc) • 108 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/http2/core.js
import {
assertCrypto,
customInspectSymbol as kInspect,
kEmptyObject,
promisify,
} from "nstdlib/lib/internal/util";
import * as assert from "nstdlib/lib/assert";
import EventEmitter from "nstdlib/lib/events";
import { addAbortListener } from "nstdlib/lib/internal/events/abort_listener";
import * as fs from "nstdlib/lib/fs";
import * as http from "nstdlib/lib/http";
import { readUInt16BE, readUInt32BE } from "nstdlib/lib/internal/buffer";
import { URL, getURLOrigin } from "nstdlib/lib/internal/url";
import * as net from "nstdlib/lib/net";
import { Duplex } from "nstdlib/lib/stream";
import * as tls from "nstdlib/lib/tls";
import { setImmediate, setTimeout, clearTimeout } from "nstdlib/lib/timers";
import {
kIncomingMessage,
_checkIsHttpToken as checkIsHttpToken,
} from "nstdlib/lib/_http_common";
import {
kServerResponse,
Server as HttpServer,
httpServerPreClose,
setupConnectionsTracking,
} from "nstdlib/lib/_http_server";
import * as JSStreamSocket from "nstdlib/lib/internal/js_stream_socket";
import {
defaultTriggerAsyncIdScope,
symbols as __symbols__,
} from "nstdlib/lib/internal/async_hooks";
import {
AbortError,
aggregateTwoErrors,
codes as __codes__,
hideStackFrames,
} from "nstdlib/lib/internal/errors";
import {
isUint32,
validateAbortSignal,
validateBoolean,
validateBuffer,
validateFunction,
validateInt32,
validateInteger,
validateNumber,
validateString,
validateUint32,
} from "nstdlib/lib/internal/validators";
import * as fsPromisesInternal from "nstdlib/lib/internal/fs/promises";
import { utcDate } from "nstdlib/lib/internal/http";
import {
Http2ServerRequest,
Http2ServerResponse,
onServerStream,
} from "nstdlib/lib/internal/http2/compat";
import {
assertIsObject,
assertIsArray,
assertValidPseudoHeader,
assertValidPseudoHeaderResponse,
assertValidPseudoHeaderTrailer,
assertWithinRange,
getAuthority,
getDefaultSettings,
getSessionState,
getSettings,
getStreamState,
isPayloadMeaningless,
kSensitiveHeaders,
kSocket,
kRequest,
kProxySocket,
mapToHeaders,
MAX_ADDITIONAL_SETTINGS,
NghttpError,
remoteCustomSettingsToBuffer,
sessionName,
toHeaderObject,
updateOptionsBuffer,
updateSettingsBuffer,
} from "nstdlib/lib/internal/http2/util";
import {
writeGeneric,
writevGeneric,
onStreamRead,
kAfterAsyncWrite,
kMaybeDestroy,
kUpdateTimer,
kHandle,
kSession,
kBoundSession,
setStreamTimeout,
} from "nstdlib/lib/internal/stream_base_commons";
import { kTimeout } from "nstdlib/lib/internal/timers";
import { isArrayBufferView } from "nstdlib/lib/internal/util/types";
import { format } from "nstdlib/lib/internal/util/inspect";
import { FileHandle } from "nstdlib/stub/binding/fs";
import * as binding from "nstdlib/stub/binding/http2";
import {
ShutdownWrap,
kReadBytesOrError,
streamBaseState,
} from "nstdlib/stub/binding/stream_wrap";
import { UV_EOF } from "nstdlib/stub/binding/uv";
import { StreamPipe } from "nstdlib/stub/binding/stream_pipe";
assertCrypto();
const { async_id_symbol, owner_symbol } = __symbols__;
const {
ERR_HTTP2_ALTSVC_INVALID_ORIGIN,
ERR_HTTP2_ALTSVC_LENGTH,
ERR_HTTP2_CONNECT_AUTHORITY,
ERR_HTTP2_CONNECT_PATH,
ERR_HTTP2_CONNECT_SCHEME,
ERR_HTTP2_GOAWAY_SESSION,
ERR_HTTP2_HEADERS_AFTER_RESPOND,
ERR_HTTP2_HEADERS_SENT,
ERR_HTTP2_INVALID_INFO_STATUS,
ERR_HTTP2_INVALID_ORIGIN,
ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH,
ERR_HTTP2_INVALID_SESSION,
ERR_HTTP2_INVALID_SETTING_VALUE,
ERR_HTTP2_INVALID_STREAM,
ERR_HTTP2_MAX_PENDING_SETTINGS_ACK,
ERR_HTTP2_NESTED_PUSH,
ERR_HTTP2_NO_MEM,
ERR_HTTP2_NO_SOCKET_MANIPULATION,
ERR_HTTP2_ORIGIN_LENGTH,
ERR_HTTP2_OUT_OF_STREAMS,
ERR_HTTP2_PAYLOAD_FORBIDDEN,
ERR_HTTP2_PING_CANCEL,
ERR_HTTP2_PING_LENGTH,
ERR_HTTP2_PUSH_DISABLED,
ERR_HTTP2_SEND_FILE,
ERR_HTTP2_SEND_FILE_NOSEEK,
ERR_HTTP2_SESSION_ERROR,
ERR_HTTP2_SETTINGS_CANCEL,
ERR_HTTP2_SOCKET_BOUND,
ERR_HTTP2_SOCKET_UNBOUND,
ERR_HTTP2_STATUS_101,
ERR_HTTP2_STATUS_INVALID,
ERR_HTTP2_STREAM_CANCEL,
ERR_HTTP2_STREAM_ERROR,
ERR_HTTP2_STREAM_SELF_DEPENDENCY,
ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS,
ERR_HTTP2_TRAILERS_ALREADY_SENT,
ERR_HTTP2_TRAILERS_NOT_READY,
ERR_HTTP2_UNSUPPORTED_PROTOCOL,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_CHAR,
ERR_INVALID_HTTP_TOKEN,
ERR_OUT_OF_RANGE,
ERR_SOCKET_CLOSED,
} = __codes__;
const { _connectionListener: httpConnectionListener } = http;
const debugEnabled = debug.enabled;
function debugStream(id, sessionType, message, ...args) {
if (!debugEnabled) {
return;
}
{
/* debug */
}
}
function debugStreamObj(stream, message, ...args) {
const session = stream[kSession];
const type = session ? session[kType] : undefined;
debugStream(stream[kID], type, message, ...args);
}
function debugSession(sessionType, message, ...args) {
{
/* debug */
}
}
function debugSessionObj(session, message, ...args) {
debugSession(session[kType], message, ...args);
}
const kMaxFrameSize = 2 ** 24 - 1;
const kMaxInt = 2 ** 32 - 1;
const kMaxStreams = 2 ** 32 - 1;
const kMaxALTSVC = 2 ** 14 - 2;
// eslint-disable-next-line no-control-regex
const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/;
const { constants, nameForErrorCode } = binding;
const NETServer = net.Server;
const TLSServer = tls.Server;
const kAlpnProtocol = Symbol("alpnProtocol");
const kAuthority = Symbol("authority");
const kEncrypted = Symbol("encrypted");
const kID = Symbol("id");
const kInit = Symbol("init");
const kInfoHeaders = Symbol("sent-info-headers");
const kLocalSettings = Symbol("local-settings");
const kNativeFields = Symbol("kNativeFields");
const kOptions = Symbol("options");
const kOwner = owner_symbol;
const kOrigin = Symbol("origin");
const kPendingRequestCalls = Symbol("kPendingRequestCalls");
const kProceed = Symbol("proceed");
const kProtocol = Symbol("protocol");
const kRemoteSettings = Symbol("remote-settings");
const kSelectPadding = Symbol("select-padding");
const kSentHeaders = Symbol("sent-headers");
const kSentTrailers = Symbol("sent-trailers");
const kServer = Symbol("server");
const kState = Symbol("state");
const kType = Symbol("type");
const kWriteGeneric = Symbol("write-generic");
const {
kBitfield,
kSessionPriorityListenerCount,
kSessionFrameErrorListenerCount,
kSessionMaxInvalidFrames,
kSessionMaxRejectedStreams,
kSessionUint8FieldCount,
kSessionHasRemoteSettingsListeners,
kSessionRemoteSettingsIsUpToDate,
kSessionHasPingListeners,
kSessionHasAltsvcListeners,
} = binding;
const {
NGHTTP2_CANCEL,
NGHTTP2_REFUSED_STREAM,
NGHTTP2_DEFAULT_WEIGHT,
NGHTTP2_FLAG_END_STREAM,
NGHTTP2_HCAT_PUSH_RESPONSE,
NGHTTP2_HCAT_RESPONSE,
NGHTTP2_INTERNAL_ERROR,
NGHTTP2_NO_ERROR,
NGHTTP2_SESSION_CLIENT,
NGHTTP2_SESSION_SERVER,
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
NGHTTP2_ERR_INVALID_ARGUMENT,
NGHTTP2_ERR_STREAM_CLOSED,
NGHTTP2_ERR_NOMEM,
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_DATE,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_PROTOCOL,
HTTP2_HEADER_SCHEME,
HTTP2_HEADER_STATUS,
HTTP2_HEADER_CONTENT_LENGTH,
NGHTTP2_SETTINGS_HEADER_TABLE_SIZE,
NGHTTP2_SETTINGS_ENABLE_PUSH,
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE,
NGHTTP2_SETTINGS_MAX_FRAME_SIZE,
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE,
NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL,
HTTP2_METHOD_GET,
HTTP2_METHOD_HEAD,
HTTP2_METHOD_CONNECT,
HTTP_STATUS_CONTINUE,
HTTP_STATUS_RESET_CONTENT,
HTTP_STATUS_OK,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_NOT_MODIFIED,
HTTP_STATUS_SWITCHING_PROTOCOLS,
HTTP_STATUS_MISDIRECTED_REQUEST,
STREAM_OPTION_EMPTY_PAYLOAD,
STREAM_OPTION_GET_TRAILERS,
} = constants;
const STREAM_FLAGS_PENDING = 0x0;
const STREAM_FLAGS_READY = 0x1;
const STREAM_FLAGS_CLOSED = 0x2;
const STREAM_FLAGS_HEADERS_SENT = 0x4;
const STREAM_FLAGS_HEAD_REQUEST = 0x8;
const STREAM_FLAGS_ABORTED = 0x10;
const STREAM_FLAGS_HAS_TRAILERS = 0x20;
const SESSION_FLAGS_PENDING = 0x0;
const SESSION_FLAGS_READY = 0x1;
const SESSION_FLAGS_CLOSED = 0x2;
const SESSION_FLAGS_DESTROYED = 0x4;
// Top level to avoid creating a closure
function emit(self, ...args) {
ReflectApply(self.emit, self, args);
}
// Called when a new block of headers has been received for a given
// stream. The stream may or may not be new. If the stream is new,
// create the associated Http2Stream instance and emit the 'stream'
// event. If the stream is not new, emit the 'headers' event to pass
// the block of headers on.
function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) {
const session = this[kOwner];
if (session.destroyed) return;
const type = session[kType];
session[kUpdateTimer]();
debugStream(id, type, "headers received");
const streams = session[kState].streams;
const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
let stream = streams.get(id);
// Convert the array of header name value pairs into an object
const obj = toHeaderObject(headers, sensitiveHeaders);
if (stream === undefined) {
if (session.closed) {
// We are not accepting any new streams at this point. This callback
// should not be invoked at this point in time, but just in case it is,
// refuse the stream using an RST_STREAM and destroy the handle.
handle.rstStream(NGHTTP2_REFUSED_STREAM);
handle.destroy();
return;
}
// session[kType] can be only one of two possible values
if (type === NGHTTP2_SESSION_SERVER) {
// eslint-disable-next-line no-use-before-define
stream = new ServerHttp2Stream(session, handle, id, {}, obj);
if (endOfStream) {
stream.push(null);
}
if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
// For head requests, there must not be a body...
// end the writable side immediately.
stream.end();
stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST;
}
} else {
// eslint-disable-next-line no-use-before-define
stream = new ClientHttp2Stream(session, handle, id, {});
if (endOfStream) {
stream.push(null);
}
stream.end();
}
if (endOfStream) stream[kState].endAfterHeaders = true;
process.nextTick(emit, session, "stream", stream, obj, flags, headers);
} else {
let event;
const status = obj[HTTP2_HEADER_STATUS];
if (cat === NGHTTP2_HCAT_RESPONSE) {
if (
!endOfStream &&
status !== undefined &&
status >= 100 &&
status < 200
) {
event = "headers";
} else {
event = "response";
}
} else if (cat === NGHTTP2_HCAT_PUSH_RESPONSE) {
event = "push";
} else if (status !== undefined && status >= 200) {
event = "response";
} else {
event = endOfStream ? "trailers" : "headers";
}
const session = stream.session;
if (status === HTTP_STATUS_MISDIRECTED_REQUEST) {
const originSet = (session[kState].originSet = initOriginSet(session));
originSet.delete(stream[kOrigin]);
}
debugStream(id, type, "emitting stream '%s' event", event);
process.nextTick(emit, stream, event, obj, flags, headers);
}
if (endOfStream) {
stream.push(null);
}
}
function tryClose(fd) {
// Try to close the file descriptor. If closing fails, assert because
// an error really should not happen at this point.
fs.close(fd, assert.ifError);
}
// Called when the Http2Stream has finished sending data and is ready for
// trailers to be sent. This will only be called if the { hasOptions: true }
// option is set.
function onStreamTrailers() {
const stream = this[kOwner];
stream[kState].trailersReady = true;
if (stream.destroyed || stream.closed) return;
if (!stream.emit("wantTrailers")) {
// There are no listeners, send empty trailing HEADERS frame and close.
stream.sendTrailers({});
}
}
// Submit an RST-STREAM frame to be sent to the remote peer.
// This will cause the Http2Stream to be closed.
function submitRstStream(code) {
if (this[kHandle] !== undefined) {
this[kHandle].rstStream(code);
}
}
// Keep track of the number/presence of JS event listeners. Knowing that there
// are no listeners allows the C++ code to skip calling into JS for an event.
function sessionListenerAdded(name) {
switch (name) {
case "ping":
this[kNativeFields][kBitfield] |= 1 << kSessionHasPingListeners;
break;
case "altsvc":
this[kNativeFields][kBitfield] |= 1 << kSessionHasAltsvcListeners;
break;
case "remoteSettings":
this[kNativeFields][kBitfield] |= 1 << kSessionHasRemoteSettingsListeners;
break;
case "priority":
this[kNativeFields][kSessionPriorityListenerCount]++;
break;
case "frameError":
this[kNativeFields][kSessionFrameErrorListenerCount]++;
break;
}
}
function sessionListenerRemoved(name) {
switch (name) {
case "ping":
if (this.listenerCount(name) > 0) return;
this[kNativeFields][kBitfield] &= ~(1 << kSessionHasPingListeners);
break;
case "altsvc":
if (this.listenerCount(name) > 0) return;
this[kNativeFields][kBitfield] &= ~(1 << kSessionHasAltsvcListeners);
break;
case "remoteSettings":
if (this.listenerCount(name) > 0) return;
this[kNativeFields][kBitfield] &= ~(
1 << kSessionHasRemoteSettingsListeners
);
break;
case "priority":
this[kNativeFields][kSessionPriorityListenerCount]--;
break;
case "frameError":
this[kNativeFields][kSessionFrameErrorListenerCount]--;
break;
}
}
// Also keep track of listeners for the Http2Stream instances, as some events
// are emitted on those objects.
function streamListenerAdded(name) {
const session = this[kSession];
if (!session) return;
switch (name) {
case "priority":
session[kNativeFields][kSessionPriorityListenerCount]++;
break;
case "frameError":
session[kNativeFields][kSessionFrameErrorListenerCount]++;
break;
}
}
function streamListenerRemoved(name) {
const session = this[kSession];
if (!session) return;
switch (name) {
case "priority":
session[kNativeFields][kSessionPriorityListenerCount]--;
break;
case "frameError":
session[kNativeFields][kSessionFrameErrorListenerCount]--;
break;
}
}
function onPing(payload) {
const session = this[kOwner];
if (session.destroyed) return;
session[kUpdateTimer]();
debugSessionObj(session, "new ping received");
session.emit("ping", payload);
}
// Called when the stream is closed either by sending or receiving an
// RST_STREAM frame, or through a natural end-of-stream.
// If the writable and readable sides of the stream are still open at this
// point, close them. If there is an open fd for file send, close that also.
// At this point the underlying node::http2:Http2Stream handle is no
// longer usable so destroy it also.
function onStreamClose(code) {
const stream = this[kOwner];
if (!stream || stream.destroyed) return false;
debugStreamObj(
stream,
"closed with code %d, closed %s, readable %s",
code,
stream.closed,
stream.readable,
);
if (!stream.closed) closeStream(stream, code, kNoRstStream);
stream[kState].fd = -1;
// Defer destroy we actually emit end.
if (!stream.readable || code !== NGHTTP2_NO_ERROR) {
// If errored or ended, we can destroy immediately.
stream.destroy();
} else {
// Wait for end to destroy.
stream.on("end", stream[kMaybeDestroy]);
// Push a null so the stream can end whenever the client consumes
// it completely.
stream.push(null);
// If the user hasn't tried to consume the stream (and this is a server
// session) then just dump the incoming data so that the stream can
// be destroyed.
if (
stream[kSession][kType] === NGHTTP2_SESSION_SERVER &&
!stream[kState].didRead &&
stream.readableFlowing === null
)
stream.resume();
else stream.read(0);
}
return true;
}
// Called when the remote peer settings have been updated.
// Resets the cached settings.
function onSettings() {
const session = this[kOwner];
if (session.destroyed) return;
session[kUpdateTimer]();
debugSessionObj(session, "new settings received");
session[kRemoteSettings] = undefined;
session.emit("remoteSettings", session.remoteSettings);
}
// If the stream exists, an attempt will be made to emit an event
// on the stream object itself. Otherwise, forward it on to the
// session (which may, in turn, forward it on to the server)
function onPriority(id, parent, weight, exclusive) {
const session = this[kOwner];
if (session.destroyed) return;
debugStream(
id,
session[kType],
"priority [parent: %d, weight: %d, exclusive: %s]",
parent,
weight,
exclusive,
);
const emitter = session[kState].streams.get(id) || session;
if (!emitter.destroyed) {
emitter[kUpdateTimer]();
emitter.emit("priority", id, parent, weight, exclusive);
}
}
// Called by the native layer when an error has occurred sending a
// frame. This should be exceedingly rare.
function onFrameError(id, type, code) {
const session = this[kOwner];
if (session.destroyed) return;
debugSessionObj(
session,
"error sending frame type %d on stream %d, code: %d",
type,
id,
code,
);
const emitter = session[kState].streams.get(id) || session;
emitter[kUpdateTimer]();
emitter.emit("frameError", type, code, id);
session[kState].streams.get(id).close(code);
session.close();
}
function onAltSvc(stream, origin, alt) {
const session = this[kOwner];
if (session.destroyed) return;
debugSessionObj(
session,
"altsvc received: stream: %d, origin: %s, alt: %s",
stream,
origin,
alt,
);
session[kUpdateTimer]();
session.emit("altsvc", alt, origin, stream);
}
function initOriginSet(session) {
let originSet = session[kState].originSet;
if (originSet === undefined) {
const socket = session[kSocket];
session[kState].originSet = originSet = new Set();
if (socket.servername != null) {
let originString = `https://${socket.servername}`;
if (socket.remotePort != null) originString += `:${socket.remotePort}`;
// We have to ensure that it is a properly serialized
// ASCII origin string. The socket.servername might not
// be properly ASCII encoded.
originSet.add(getURLOrigin(originString));
}
}
return originSet;
}
function onOrigin(origins) {
const session = this[kOwner];
if (session.destroyed) return;
debugSessionObj(session, "origin received: %j", origins);
session[kUpdateTimer]();
if (!session.encrypted || session.destroyed) return undefined;
const originSet = initOriginSet(session);
for (let n = 0; n < origins.length; n++) originSet.add(origins[n]);
session.emit("origin", origins);
}
// Receiving a GOAWAY frame from the connected peer is a signal that no
// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
// are going to send our close, but allow existing frames to close
// normally. If code !== NGHTTP2_NO_ERROR, we are going to send our own
// close using the same code then destroy the session with an error.
// The goaway event will be emitted on next tick.
function onGoawayData(code, lastStreamID, buf) {
const session = this[kOwner];
if (session.destroyed) return;
debugSessionObj(
session,
"goaway %d received [last stream id: %d]",
code,
lastStreamID,
);
const state = session[kState];
state.goawayCode = code;
state.goawayLastStreamID = lastStreamID;
session.emit("goaway", code, lastStreamID, buf);
if (code === NGHTTP2_NO_ERROR) {
// If this is a no error goaway, begin shutting down.
// No new streams permitted, but existing streams may
// close naturally on their own.
session.close();
} else {
// However, if the code is not NGHTTP_NO_ERROR, destroy the
// session immediately. We destroy with an error but send a
// goaway using NGHTTP2_NO_ERROR because there was no error
// condition on this side of the session that caused the
// shutdown.
session.destroy(new ERR_HTTP2_SESSION_ERROR(code), NGHTTP2_NO_ERROR);
}
}
// When a ClientHttp2Session is first created, the socket may not yet be
// connected. If request() is called during this time, the actual request
// will be deferred until the socket is ready to go.
function requestOnConnect(headers, options) {
const session = this[kSession];
// At this point, the stream should have already been destroyed during
// the session.destroy() method. Do nothing else.
if (session === undefined || session.destroyed) return;
// If the session was closed while waiting for the connect, destroy
// the stream and do not continue with the request.
if (session.closed) {
const err = new ERR_HTTP2_GOAWAY_SESSION();
this.destroy(err);
return;
}
debugSessionObj(session, "connected, initializing request");
let streamOptions = 0;
if (options.endStream) streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
if (options.waitForTrailers) streamOptions |= STREAM_OPTION_GET_TRAILERS;
// `ret` will be either the reserved stream ID (if positive)
// or an error code (if negative)
const ret = session[kHandle].request(
headers,
streamOptions,
options.parent | 0,
options.weight | 0,
!!options.exclusive,
);
// In an error condition, one of three possible response codes will be
// possible:
// * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this
// is fatal for the session
// * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this
// impacts on this stream.
// For the first two, emit the error on the session,
// For the third, emit the error on the stream, it will bubble up to the
// session if not handled.
if (typeof ret === "number") {
let err;
switch (ret) {
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
err = new ERR_HTTP2_OUT_OF_STREAMS();
this.destroy(err);
break;
case NGHTTP2_ERR_INVALID_ARGUMENT:
err = new ERR_HTTP2_STREAM_SELF_DEPENDENCY();
this.destroy(err);
break;
default:
session.destroy(new NghttpError(ret));
}
return;
}
this[kInit](ret.id(), ret);
}
// Validates that priority options are correct, specifically:
// 1. options.weight must be a number
// 2. options.parent must be a positive number
// 3. options.exclusive must be a boolean
// 4. if specified, options.silent must be a boolean
//
// Also sets the default priority options if they are not set.
const setAndValidatePriorityOptions = hideStackFrames((options) => {
if (options.weight === undefined) {
options.weight = NGHTTP2_DEFAULT_WEIGHT;
} else {
validateNumber.withoutStackTrace(options.weight, "options.weight");
}
if (options.parent === undefined) {
options.parent = 0;
} else {
validateNumber.withoutStackTrace(options.parent, "options.parent", 0);
}
if (options.exclusive === undefined) {
options.exclusive = false;
} else {
validateBoolean.withoutStackTrace(options.exclusive, "options.exclusive");
}
if (options.silent === undefined) {
options.silent = false;
} else {
validateBoolean.withoutStackTrace(options.silent, "options.silent");
}
});
// When an error occurs internally at the binding level, immediately
// destroy the session.
function onSessionInternalError(integerCode, customErrorCode) {
if (this[kOwner] !== undefined)
this[kOwner].destroy(new NghttpError(integerCode, customErrorCode));
}
function settingsCallback(cb, ack, duration) {
this[kState].pendingAck--;
this[kLocalSettings] = undefined;
if (ack) {
debugSessionObj(this, "settings received");
const settings = this.localSettings;
if (typeof cb === "function") cb(null, settings, duration);
this.emit("localSettings", settings);
} else {
debugSessionObj(this, "settings canceled");
if (typeof cb === "function") cb(new ERR_HTTP2_SETTINGS_CANCEL());
}
}
// Submits a SETTINGS frame to be sent to the remote peer.
function submitSettings(settings, callback) {
if (this.destroyed) return;
debugSessionObj(this, "submitting settings");
this[kUpdateTimer]();
updateSettingsBuffer(settings);
if (!this[kHandle].settings(settingsCallback.bind(this, callback))) {
this.destroy(new ERR_HTTP2_MAX_PENDING_SETTINGS_ACK());
}
}
// Submits a PRIORITY frame to be sent to the remote peer
// Note: If the silent option is true, the change will be made
// locally with no PRIORITY frame sent.
function submitPriority(options) {
if (this.destroyed) return;
this[kUpdateTimer]();
// If the parent is the id, do nothing because a
// stream cannot be made to depend on itself.
if (options.parent === this[kID]) return;
this[kHandle].priority(
options.parent | 0,
options.weight | 0,
!!options.exclusive,
!!options.silent,
);
}
// Submit a GOAWAY frame to be sent to the remote peer.
// If the lastStreamID is set to <= 0, then the lastProcStreamID will
// be used. The opaqueData must either be a typed array or undefined
// (which will be checked elsewhere).
function submitGoaway(code, lastStreamID, opaqueData) {
if (this.destroyed) return;
debugSessionObj(this, "submitting goaway");
this[kUpdateTimer]();
this[kHandle].goaway(code, lastStreamID, opaqueData);
}
const proxySocketHandler = {
get(session, prop) {
switch (prop) {
case "setTimeout":
case "ref":
case "unref":
return session[prop].bind(session);
case "destroy":
case "emit":
case "end":
case "pause":
case "read":
case "resume":
case "write":
case "setEncoding":
case "setKeepAlive":
case "setNoDelay":
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
const socket = session[kSocket];
if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND();
const value = socket[prop];
return typeof value === "function" ? value.bind(socket) : value;
}
}
},
getPrototypeOf(session) {
const socket = session[kSocket];
if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND();
return Reflect.getPrototypeOf(socket);
},
set(session, prop, value) {
switch (prop) {
case "setTimeout":
case "ref":
case "unref":
session[prop] = value;
return true;
case "destroy":
case "emit":
case "end":
case "pause":
case "read":
case "resume":
case "write":
case "setEncoding":
case "setKeepAlive":
case "setNoDelay":
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
const socket = session[kSocket];
if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND();
socket[prop] = value;
return true;
}
}
},
};
// pingCallback() returns a function that is invoked when an HTTP2 PING
// frame acknowledgement is received. The ack is either true or false to
// indicate if the ping was successful or not. The duration indicates the
// number of milliseconds elapsed since the ping was sent and the ack
// received. The payload is a Buffer containing the 8 bytes of payload
// data received on the PING acknowledgement.
function pingCallback(cb) {
return function pingCallback(ack, duration, payload) {
if (ack) {
cb(null, duration, payload);
} else {
cb(new ERR_HTTP2_PING_CANCEL());
}
};
}
// Validates the values in a settings object. Specifically:
// 1. headerTableSize must be a number in the range 0 <= n <= kMaxInt
// 2. initialWindowSize must be a number in the range 0 <= n <= kMaxInt
// 3. maxFrameSize must be a number in the range 16384 <= n <= kMaxFrameSize
// 4. maxConcurrentStreams must be a number in the range 0 <= n <= kMaxStreams
// 5. maxHeaderListSize must be a number in the range 0 <= n <= kMaxInt
// 6. enablePush must be a boolean
// 7. enableConnectProtocol must be a boolean
// All settings are optional and may be left undefined
const validateSettings = hideStackFrames((settings) => {
if (settings === undefined) return;
assertIsObject.withoutStackTrace(
settings.customSettings,
"customSettings",
"Number",
);
if (settings.customSettings) {
const entries = Object.entries(settings.customSettings);
if (entries.length > MAX_ADDITIONAL_SETTINGS)
throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
for (const { 0: key, 1: value } of entries) {
assertWithinRange.withoutStackTrace(
"customSettings:id",
Number(key),
0,
0xffff,
);
assertWithinRange.withoutStackTrace(
"customSettings:value",
Number(value),
0,
kMaxInt,
);
}
}
assertWithinRange.withoutStackTrace(
"headerTableSize",
settings.headerTableSize,
0,
kMaxInt,
);
assertWithinRange.withoutStackTrace(
"initialWindowSize",
settings.initialWindowSize,
0,
kMaxInt,
);
assertWithinRange.withoutStackTrace(
"maxFrameSize",
settings.maxFrameSize,
16384,
kMaxFrameSize,
);
assertWithinRange.withoutStackTrace(
"maxConcurrentStreams",
settings.maxConcurrentStreams,
0,
kMaxStreams,
);
assertWithinRange.withoutStackTrace(
"maxHeaderListSize",
settings.maxHeaderListSize,
0,
kMaxInt,
);
assertWithinRange.withoutStackTrace(
"maxHeaderSize",
settings.maxHeaderSize,
0,
kMaxInt,
);
if (
settings.enablePush !== undefined &&
typeof settings.enablePush !== "boolean"
) {
throw new ERR_HTTP2_INVALID_SETTING_VALUE.HideStackFramesError(
"enablePush",
settings.enablePush,
);
}
if (
settings.enableConnectProtocol !== undefined &&
typeof settings.enableConnectProtocol !== "boolean"
) {
throw new ERR_HTTP2_INVALID_SETTING_VALUE.HideStackFramesError(
"enableConnectProtocol",
settings.enableConnectProtocol,
);
}
});
// Wrap a typed array in a proxy, and allow selectively copying the entries
// that have explicitly been set to another typed array.
function trackAssignmentsTypedArray(typedArray) {
const typedArrayLength = typedArray.length;
const modifiedEntries = new Uint8Array(typedArrayLength);
function copyAssigned(target) {
for (let i = 0; i < typedArrayLength; i++) {
if (modifiedEntries[i]) {
target[i] = typedArray[i];
}
}
}
return new Proxy(typedArray, {
__proto__: null,
get(obj, prop, receiver) {
if (prop === "copyAssigned") {
return copyAssigned;
}
return ReflectGet(obj, prop, receiver);
},
set(obj, prop, value) {
if (`${+prop}` === prop) {
modifiedEntries[prop] = 1;
}
return ReflectSet(obj, prop, value);
},
});
}
// Creates the internal binding.Http2Session handle for an Http2Session
// instance. This occurs only after the socket connection has been
// established. Note: the binding.Http2Session will take over ownership
// of the socket. No other code should read from or write to the socket.
function setupHandle(socket, type, options) {
// If the session has been destroyed, go ahead and emit 'connect',
// but do nothing else. The various on('connect') handlers set by
// core will check for session.destroyed before progressing, this
// ensures that those at least get cleared out.
if (this.destroyed) {
process.nextTick(emit, this, "connect", this, socket);
return;
}
assert(
socket._handle !== undefined,
"Internal HTTP/2 Failure. The socket is not connected. Please " +
"report this as a bug in Node.js",
);
debugSession(type, "setting up session handle");
this[kState].flags |= SESSION_FLAGS_READY;
updateOptionsBuffer(options);
if (options.remoteCustomSettings) {
remoteCustomSettingsToBuffer(options.remoteCustomSettings);
}
const handle = new binding.Http2Session(type);
handle[kOwner] = this;
if (typeof options.selectPadding === "function")
this[kSelectPadding] = options.selectPadding;
handle.consume(socket._handle);
this[kHandle] = handle;
if (this[kNativeFields]) {
// If some options have already been set before the handle existed, copy
// those (and only those) that have manually been set over.
this[kNativeFields].copyAssigned(handle.fields);
}
this[kNativeFields] = handle.fields;
if (socket.encrypted) {
this[kAlpnProtocol] = socket.alpnProtocol;
this[kEncrypted] = true;
} else {
// 'h2c' is the protocol identifier for HTTP/2 over plain-text. We use
// it here to identify any session that is not explicitly using an
// encrypted socket.
this[kAlpnProtocol] = "h2c";
this[kEncrypted] = false;
}
if (isUint32(options.maxSessionInvalidFrames)) {
const uint32 = new Uint32Array(
this[kNativeFields].buffer,
kSessionMaxInvalidFrames,
1,
);
uint32[0] = options.maxSessionInvalidFrames;
}
if (isUint32(options.maxSessionRejectedStreams)) {
const uint32 = new Uint32Array(
this[kNativeFields].buffer,
kSessionMaxRejectedStreams,
1,
);
uint32[0] = options.maxSessionRejectedStreams;
}
const settings = typeof options.settings === "object" ? options.settings : {};
this.settings(settings);
if (type === NGHTTP2_SESSION_SERVER && Array.isArray(options.origins)) {
ReflectApply(this.origin, this, options.origins);
}
process.nextTick(emit, this, "connect", this, socket);
}
// Emits a close event followed by an error event if err is truthy. Used
// by Http2Session.prototype.destroy()
function emitClose(self, error) {
if (error) self.emit("error", error);
self.emit("close");
}
function cleanupSession(session) {
const socket = session[kSocket];
const handle = session[kHandle];
session[kProxySocket] = undefined;
session[kSocket] = undefined;
session[kHandle] = undefined;
session[kNativeFields] = trackAssignmentsTypedArray(
new Uint8Array(kSessionUint8FieldCount),
);
if (handle) handle.ondone = null;
if (socket) {
socket[kBoundSession] = undefined;
socket[kServer] = undefined;
}
}
function finishSessionClose(session, error) {
debugSessionObj(session, "finishSessionClose");
const socket = session[kSocket];
cleanupSession(session);
if (socket && !socket.destroyed) {
socket.on("close", () => {
emitClose(session, error);
});
if (session.closed) {
// If we're gracefully closing the socket, call resume() so we can detect
// the peer closing in case binding.Http2Session is already gone.
socket.resume();
}
// Always wait for writable side to finish.
socket.end((err) => {
debugSessionObj(session, "finishSessionClose socket end", err, error);
// If session.destroy() was called, destroy the underlying socket. Delay
// it a bit to try to avoid ECONNRESET on Windows.
if (!session.closed) {
setImmediate(() => {
socket.destroy(error);
});
}
});
} else {
process.nextTick(emitClose, session, error);
}
}
function closeSession(session, code, error) {
debugSessionObj(session, "start closing/destroying", error);
const state = session[kState];
state.flags |= SESSION_FLAGS_DESTROYED;
state.destroyCode = code;
// Clear timeout and remove timeout listeners.
session.setTimeout(0);
session.removeAllListeners("timeout");
// Destroy any pending and open streams
if (state.pendingStreams.size > 0 || state.streams.size > 0) {
const cancel = new ERR_HTTP2_STREAM_CANCEL(error);
state.pendingStreams.forEach((stream) => stream.destroy(cancel));
state.streams.forEach((stream) => stream.destroy(error));
}
// Disassociate from the socket and server.
const socket = session[kSocket];
const handle = session[kHandle];
// Destroy the handle if it exists at this point.
if (handle !== undefined) {
handle.ondone = finishSessionClose.bind(null, session, error);
handle.destroy(code, socket.destroyed);
} else {
finishSessionClose(session, error);
}
}
// Upon creation, the Http2Session takes ownership of the socket. The session
// may not be ready to use immediately if the socket is not yet fully connected.
// In that case, the Http2Session will wait for the socket to connect. Once
// the Http2Session is ready, it will emit its own 'connect' event.
//
// The Http2Session.goaway() method will send a GOAWAY frame, signalling
// to the connected peer that a shutdown is in progress. Sending a goaway
// frame has no other effect, however.
//
// Receiving a GOAWAY frame will cause the Http2Session to first emit a 'goaway'
// event notifying the user that a shutdown is in progress. If the goaway
// error code equals 0 (NGHTTP2_NO_ERROR), session.close() will be called,
// causing the Http2Session to send its own GOAWAY frame and switch itself
// into a graceful closing state. In this state, new inbound or outbound
// Http2Streams will be rejected. Existing *pending* streams (those created
// but without an assigned stream ID or handle) will be destroyed with a
// cancel error. Existing open streams will be permitted to complete on their
// own. Once all existing streams close, session.destroy() will be called
// automatically.
//
// Calling session.destroy() will tear down the Http2Session immediately,
// making it no longer usable. Pending and existing streams will be destroyed.
// The bound socket will be destroyed. Once all resources have been freed up,
// the 'close' event will be emitted. Note that pending streams will be
// destroyed using a specific "ERR_HTTP2_STREAM_CANCEL" error. Existing open
// streams will be destroyed using the same error passed to session.destroy()
//
// If destroy is called with an error, an 'error' event will be emitted
// immediately following the 'close' event.
//
// The socket and Http2Session lifecycles are tightly bound. Once one is
// destroyed, the other should also be destroyed. When the socket is destroyed
// with an error, session.destroy() will be called with that same error.
// Likewise, when session.destroy() is called with an error, the same error
// will be sent to the socket.
class Http2Session extends EventEmitter {
constructor(type, options, socket) {
super();
// No validation is performed on the input parameters because this
// constructor is not exported directly for users.
// If the session property already exists on the socket,
// then it has already been bound to an Http2Session instance
// and cannot be attached again.
if (socket[kBoundSession] !== undefined) throw new ERR_HTTP2_SOCKET_BOUND();
socket[kBoundSession] = this;
if (!socket._handle || !socket._handle.isStreamBase) {
socket = new JSStreamSocket(socket);
}
socket.on("error", socketOnError);
socket.on("close", socketOnClose);
this[kState] = {
destroyCode: NGHTTP2_NO_ERROR,
flags: SESSION_FLAGS_PENDING,
goawayCode: null,
goawayLastStreamID: null,
streams: new Map(),
pendingStreams: new Set(),
pendingAck: 0,
shutdownWritableCalled: false,
writeQueueSize: 0,
originSet: undefined,
};
this[kEncrypted] = undefined;
this[kAlpnProtocol] = undefined;
this[kType] = type;
this[kProxySocket] = null;
this[kSocket] = socket;
this[kTimeout] = null;
this[kHandle] = undefined;
// Do not use nagle's algorithm
if (typeof socket.setNoDelay === "function") socket.setNoDelay();
// Disable TLS renegotiation on the socket
if (typeof socket.disableRenegotiation === "function")
socket.disableRenegotiation();
const setupFn = setupHandle.bind(this, socket, type, options);
if (socket.connecting || socket.secureConnecting) {
const connectEvent =
socket instanceof tls.TLSSocket ? "secureConnect" : "connect";
socket.once(connectEvent, () => {
try {
setupFn();
} catch (error) {
socket.destroy(error);
}
});
} else {
setupFn();
}
if (!this[kNativeFields]) {
this[kNativeFields] = trackAssignmentsTypedArray(
new Uint8Array(kSessionUint8FieldCount),
);
}
this.on("newListener", sessionListenerAdded);
this.on("removeListener", sessionListenerRemoved);
// Process data on the next tick - a remoteSettings handler may be attached.
// https://github.com/nodejs/node/issues/35981
process.nextTick(() => {
// Socket already has some buffered data - emulate receiving it
// https://github.com/nodejs/node/issues/35475
// https://github.com/nodejs/node/issues/34532
if (socket.readableLength) {
let buf;
while ((buf = socket.read()) !== null) {
debugSession(type, `${buf.length} bytes already in buffer`);
this[kHandle].receive(buf);
}
}
});
debugSession(type, "created");
}
// Returns undefined if the socket is not yet connected, true if the
// socket is a TLSSocket, and false if it is not.
get encrypted() {
return this[kEncrypted];
}
// Returns undefined if the socket is not yet connected, `h2` if the
// socket is a TLSSocket and the alpnProtocol is `h2`, or `h2c` if the
// socket is not a TLSSocket.
get alpnProtocol() {
return this[kAlpnProtocol];
}
// TODO(jasnell): originSet is being added in preparation for ORIGIN frame
// support. At the current time, the ORIGIN frame specification is awaiting
// publication as an RFC and is awaiting implementation in nghttp2. Once
// added, an ORIGIN frame will add to the origins included in the origin
// set. 421 responses will remove origins from the set.
get originSet() {
if (!this.encrypted || this.destroyed) return undefined;
return Array.from(initOriginSet(this));
}
// True if the Http2Session is still waiting for the socket to connect
get connecting() {
return (this[kState].flags & SESSION_FLAGS_READY) === 0;
}
// True if Http2Session.prototype.close() has been called
get closed() {
return !!(this[kState].flags & SESSION_FLAGS_CLOSED);
}
// True if Http2Session.prototype.destroy() has been called
get destroyed() {
return !!(this[kState].flags & SESSION_FLAGS_DESTROYED);
}
// Resets the timeout counter
[kUpdateTimer]() {
if (this.destroyed) return;
if (this[kTimeout]) this[kTimeout].refresh();
}
// Sets the id of the next stream to be created by this Http2Session.
// The value must be a number in the range 0 <= n <= kMaxStreams. The
// value also needs to be larger than the current next stream ID.
setNextStreamID(id) {
if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION();
validateNumber(id, "id");
if (id <= 0 || id > kMaxStreams)
throw new ERR_OUT_OF_RANGE("id", `> 0 and <= ${kMaxStreams}`, id);
this[kHandle].setNextStreamID(id);
}
// Sets the local window size (local endpoints's window size)
// Returns 0 if success or throw an exception if NGHTTP2_ERR_NOMEM
// if the window allocation fails
setLocalWindowSize(windowSize) {
if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION();
validateInt32(windowSize, "windowSize", 0);
const ret = this[kHandle].setLocalWindowSize(windowSize);
if (ret === NGHTTP2_ERR_NOMEM) {
this.destroy(new ERR_HTTP2_NO_MEM());
}
}
// If ping is called while we are still connecting, or after close() has
// been called, the ping callback will be invoked immediately with a ping
// cancelled error and a duration of 0.0.
ping(payload, callback) {
if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION();
if (typeof payload === "function") {
callback = payload;
payload = undefined;
}
if (payload) {
validateBuffer(payload, "payload");
}
if (payload && payload.length !== 8) {
throw new ERR_HTTP2_PING_LENGTH();
}
validateFunction(callback, "callback");
const cb = pingCallback(callback);
if (this.connecting || this.closed) {
process.nextTick(cb, false, 0.0, payload);
return;
}
return this[kHandle].ping(payload, cb);
}
[kInspect](depth, opts) {
if (typeof depth === "number" && depth < 0) return this;
const obj = {
type: this[kType],
closed: this.closed,
destroyed: this.destroyed,
state: this.state,
localSettings: this.localSettings,
remoteSettings: this.remoteSettings,
};
return `Http2Session ${format(obj)}`;
}
// The socket owned by this session
get socket() {
const proxySocket = this[kProxySocket];
if (proxySocket === null)
return (this[kProxySocket] = new Proxy(this, proxySocketHandler));
return proxySocket;
}
// The session type
get type() {
return this[kType];
}
// If a GOAWAY frame has been received, gives the error code specified
get goawayCode() {
return this[kState].goawayCode || NGHTTP2_NO_ERROR;
}
// If a GOAWAY frame has been received, gives the last stream ID reported
get goawayLastStreamID() {
return this[kState].goawayLastStreamID || 0;
}
// True if the Http2Session is waiting for a settings acknowledgement
get pendingSettingsAck() {
return this[kState].pendingAck > 0;
}
// Retrieves state information for the Http2Session
get state() {
return this.connecting || this.destroyed
? {}
: getSessionState(this[kHandle]);
}
// The settings currently in effect for the local peer. These will
// be updated only when a settings acknowledgement has been received.
get localSettings() {
const settings = this[kLocalSettings];
if (settings !== undefined) return settings;
if (this.destroyed || this.connecting) return {};
return (this[kLocalSettings] = getSettings(this[kHandle], false)); // Local
}
// The settings currently in effect for the remote peer.
get remoteSettings() {
if (
this[kNativeFields][kBitfield] &
(1 << kSessionRemoteSettingsIsUpToDate)
) {
const settings = this[kRemoteSettings];
if (settings !== undefined) {
return settings;
}
}
if (this.destroyed || this.connecting) return {};
this[kNativeFields][kBitfield] |= 1 << kSessionRemoteSettingsIsUpToDate;
return (this[kRemoteSettings] = getSettings(this[kHandle], true)); // Remote
}
// Submits a SETTINGS frame to be sent to the remote peer.
settings(settings, callback) {
if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION();
assertIsObject(settings, "settings");
validateSettings(settings);
if (callback) {
validateFunction(callback, "callback");
}
debugSessionObj(this, "sending settings");
this[kState].pendingAck++;
const settingsFn = submitSettings.bind(this, { ...settings }, callback);
if (this.connecting) {
this.once("connect", settingsFn);
return;
}
settingsFn();
}
// Submits a GOAWAY frame to be sent to the remote peer. Note that this
// is only a notification, and does not affect the usable state of the
// session with the notable exception that new incoming streams will
// be rejected automatically.
goaway(code = NGHTTP2_NO_ERROR, lastStreamID = 0, opaqueData) {
if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION();
if (opaqueData !== undefined) {
validateBuffer(opaqueData, "opaqueData");
}
validateNumber(code, "code");
validateNumber(lastStreamID, "lastStreamID");
const goawayFn = submitGoaway.bind(this, code, lastStreamID, opaqueData);
if (this.connecting) {
this.once("connect", goawayFn);
return;
}
goawayFn();
}
// Destroy the Http2Session, making it no longer usable and cancelling
// any pending activity.
destroy(error = NGHTTP2_NO_ERROR, code) {
if (this.destroyed) return;
debugSessionObj(this, "destroying");
if (typeof error === "number") {
code = error;
error =
code !== NGHTTP2_NO_ERROR
? new ERR_HTTP2_SESSION_ERROR(code)
: undefined;
}
if (code === undefined && error != null) code = NGHTTP2_INTERNAL_ERROR;
closeSession(this, code, error);
}
// Closing the session will:
// 1. Send a goaway frame
// 2. Mark the session as closed
// 3. Prevent new inbound or outbound streams from being opened
// 4. Optionally register a 'close' event handler
// 5. Will cause the session to automatically destroy after the
// last currently open Http2Stream closes.
//
// Close always assumes a good, non-error shutdown (NGHTTP_NO_ERROR)
//
// If the session has not connected yet, the closed flag will still be
// set but the goaway will not be sent until after the connect event
// is emitted.
close(callback) {
if (this.closed || this.destroyed) return;
debugSessionObj(this, "marking session closed");
this[kState].flags |= SESSION_FLAGS_CLOSED;
if (typeof callback === "function") this.once("close", callback);
this.goaway();
this[kMaybeDestroy]();
}
[EventEmitter.captureRejectionSymbol](err, event, ...args) {
switch (event) {
case "stream": {
const stream = args[0];
stream.destroy(err);
break;
}
default:
this.destroy(err);
}
}
// Destroy the session if:
// * error is not undefined/null
// * session is closed and there are no more pending or open streams
[kMaybeDestroy](error) {
if (error == null) {
const state = this[kState];
// Do not destroy if we're not closed and there are pending/open streams
if (
!this.closed ||
state.streams