http2: adjust error emit in core, add tests
Use the ability of nextTick and setImmediate to pass arguments instead of creating closures or binding. Add tests that cover the vast majority of error emits. PR-URL: https://github.com/nodejs/node/pull/15586 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
parent
dcd890a135
commit
ccd3afc843
@ -130,8 +130,8 @@ const {
|
|||||||
} = constants;
|
} = constants;
|
||||||
|
|
||||||
// Top level to avoid creating a closure
|
// Top level to avoid creating a closure
|
||||||
function emit() {
|
function emit(self, ...args) {
|
||||||
this.emit.apply(this, arguments);
|
self.emit(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a new block of headers has been received for a given
|
// Called when a new block of headers has been received for a given
|
||||||
@ -169,7 +169,7 @@ function onSessionHeaders(id, cat, flags, headers) {
|
|||||||
stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
|
stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
|
||||||
}
|
}
|
||||||
streams.set(id, stream);
|
streams.set(id, stream);
|
||||||
process.nextTick(emit.bind(owner, 'stream', stream, obj, flags, headers));
|
process.nextTick(emit, owner, 'stream', stream, obj, flags, headers);
|
||||||
} else {
|
} else {
|
||||||
let event;
|
let event;
|
||||||
const status = obj[HTTP2_HEADER_STATUS];
|
const status = obj[HTTP2_HEADER_STATUS];
|
||||||
@ -192,7 +192,7 @@ function onSessionHeaders(id, cat, flags, headers) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug(`[${sessionName(owner[kType])}] emitting stream '${event}' event`);
|
debug(`[${sessionName(owner[kType])}] emitting stream '${event}' event`);
|
||||||
process.nextTick(emit.bind(stream, event, obj, flags, headers));
|
process.nextTick(emit, stream, event, obj, flags, headers);
|
||||||
}
|
}
|
||||||
if (endOfStream) {
|
if (endOfStream) {
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
@ -224,7 +224,7 @@ function onSessionTrailers(id) {
|
|||||||
getTrailers.call(stream, trailers);
|
getTrailers.call(stream, trailers);
|
||||||
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
|
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
|
||||||
if (!Array.isArray(headersList)) {
|
if (!Array.isArray(headersList)) {
|
||||||
process.nextTick(() => stream.emit('error', headersList));
|
process.nextTick(emit, stream, 'error', headersList);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return headersList;
|
return headersList;
|
||||||
@ -258,14 +258,14 @@ function onSessionStreamClose(id, code) {
|
|||||||
|
|
||||||
function afterFDClose(err) {
|
function afterFDClose(err) {
|
||||||
if (err)
|
if (err)
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when an error event needs to be triggered
|
// Called when an error event needs to be triggered
|
||||||
function onSessionError(error) {
|
function onSessionError(error) {
|
||||||
const owner = this[kOwner];
|
const owner = this[kOwner];
|
||||||
_unrefActive(owner);
|
_unrefActive(owner);
|
||||||
process.nextTick(() => owner.emit('error', error));
|
process.nextTick(emit, owner, 'error', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receives a chunk of data for a given stream and forwards it on
|
// Receives a chunk of data for a given stream and forwards it on
|
||||||
@ -314,7 +314,7 @@ function onSettings(ack) {
|
|||||||
if (owner.listenerCount(event) > 0) {
|
if (owner.listenerCount(event) > 0) {
|
||||||
const settings = event === 'localSettings' ?
|
const settings = event === 'localSettings' ?
|
||||||
owner.localSettings : owner.remoteSettings;
|
owner.localSettings : owner.remoteSettings;
|
||||||
process.nextTick(emit.bind(owner, event, settings));
|
process.nextTick(emit, owner, event, settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,15 +330,14 @@ function onPriority(id, parent, weight, exclusive) {
|
|||||||
const streams = owner[kState].streams;
|
const streams = owner[kState].streams;
|
||||||
const stream = streams.get(id);
|
const stream = streams.get(id);
|
||||||
const emitter = stream === undefined ? owner : stream;
|
const emitter = stream === undefined ? owner : stream;
|
||||||
process.nextTick(
|
process.nextTick(emit, emitter, 'priority', id, parent, weight, exclusive);
|
||||||
emit.bind(emitter, 'priority', id, parent, weight, exclusive));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitFrameError(id, type, code) {
|
function emitFrameError(self, id, type, code) {
|
||||||
if (!this.emit('frameError', type, code, id)) {
|
if (!self.emit('frameError', type, code, id)) {
|
||||||
const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id);
|
const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id);
|
||||||
err.errno = code;
|
err.errno = code;
|
||||||
this.emit('error', err);
|
self.emit('error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,27 +351,27 @@ function onFrameError(id, type, code) {
|
|||||||
const streams = owner[kState].streams;
|
const streams = owner[kState].streams;
|
||||||
const stream = streams.get(id);
|
const stream = streams.get(id);
|
||||||
const emitter = stream !== undefined ? stream : owner;
|
const emitter = stream !== undefined ? stream : owner;
|
||||||
process.nextTick(emitFrameError.bind(emitter, id, type, code));
|
process.nextTick(emitFrameError, emitter, id, type, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitGoaway(state, code, lastStreamID, buf) {
|
function emitGoaway(self, code, lastStreamID, buf) {
|
||||||
this.emit('goaway', code, lastStreamID, buf);
|
self.emit('goaway', code, lastStreamID, buf);
|
||||||
// Tear down the session or destroy
|
// Tear down the session or destroy
|
||||||
|
const state = self[kState];
|
||||||
if (state.destroying || state.destroyed)
|
if (state.destroying || state.destroyed)
|
||||||
return;
|
return;
|
||||||
if (!state.shuttingDown && !state.shutdown) {
|
if (!state.shuttingDown && !state.shutdown) {
|
||||||
this.shutdown({}, this.destroy.bind(this));
|
self.shutdown({}, self.destroy.bind(self));
|
||||||
} else {
|
} else {
|
||||||
this.destroy();
|
self.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by the native layer when a goaway frame has been received
|
// Called by the native layer when a goaway frame has been received
|
||||||
function onGoawayData(code, lastStreamID, buf) {
|
function onGoawayData(code, lastStreamID, buf) {
|
||||||
const owner = this[kOwner];
|
const owner = this[kOwner];
|
||||||
const state = owner[kState];
|
|
||||||
debug(`[${sessionName(owner[kType])}] goaway data received`);
|
debug(`[${sessionName(owner[kType])}] goaway data received`);
|
||||||
process.nextTick(emitGoaway.bind(owner, state, code, lastStreamID, buf));
|
process.nextTick(emitGoaway, owner, code, lastStreamID, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the padding to use per frame. The selectPadding callback is set
|
// Returns the padding to use per frame. The selectPadding callback is set
|
||||||
@ -408,7 +407,7 @@ function requestOnConnect(headers, options) {
|
|||||||
|
|
||||||
const headersList = mapToHeaders(headers);
|
const headersList = mapToHeaders(headers);
|
||||||
if (!Array.isArray(headersList)) {
|
if (!Array.isArray(headersList)) {
|
||||||
process.nextTick(() => this.emit('error', headersList));
|
process.nextTick(emit, this, 'error', headersList);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,21 +440,21 @@ function requestOnConnect(headers, options) {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
||||||
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
case NGHTTP2_ERR_INVALID_ARGUMENT:
|
case NGHTTP2_ERR_INVALID_ARGUMENT:
|
||||||
err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
|
err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Some other, unexpected error was returned. Emit on the session.
|
// Some other, unexpected error was returned. Emit on the session.
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
debug(`[${sessionName(session[kType])}] stream ${ret} initialized`);
|
debug(`[${sessionName(session[kType])}] stream ${ret} initialized`);
|
||||||
@ -542,7 +541,7 @@ function setupHandle(session, socket, type, options) {
|
|||||||
options.settings : Object.create(null);
|
options.settings : Object.create(null);
|
||||||
|
|
||||||
session.settings(settings);
|
session.settings(settings);
|
||||||
process.nextTick(emit.bind(session, 'connect', session, socket));
|
process.nextTick(emit, session, 'connect', session, socket);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -559,13 +558,13 @@ function submitSettings(settings) {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Some other unexpected error was reported.
|
// Some other unexpected error was reported.
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug(`[${sessionName(this[kType])}] settings complete`);
|
debug(`[${sessionName(this[kType])}] settings complete`);
|
||||||
@ -591,13 +590,13 @@ function submitPriority(stream, options) {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Some other unexpected error was reported.
|
// Some other unexpected error was reported.
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, stream, 'error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug(`[${sessionName(this[kType])}] priority complete`);
|
debug(`[${sessionName(this[kType])}] priority complete`);
|
||||||
@ -615,13 +614,13 @@ function submitRstStream(stream, code) {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Some other unexpected error was reported.
|
// Some other unexpected error was reported.
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, stream, 'error', err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
@ -629,9 +628,9 @@ function submitRstStream(stream, code) {
|
|||||||
debug(`[${sessionName(this[kType])}] rststream complete`);
|
debug(`[${sessionName(this[kType])}] rststream complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function doShutdown(options) {
|
function doShutdown(self, options) {
|
||||||
const handle = this[kHandle];
|
const handle = self[kHandle];
|
||||||
const state = this[kState];
|
const state = self[kState];
|
||||||
if (handle === undefined || state.shutdown)
|
if (handle === undefined || state.shutdown)
|
||||||
return; // Nothing to do, possibly because the session shutdown already.
|
return; // Nothing to do, possibly because the session shutdown already.
|
||||||
const ret = handle.submitGoaway(options.errorCode | 0,
|
const ret = handle.submitGoaway(options.errorCode | 0,
|
||||||
@ -640,13 +639,13 @@ function doShutdown(options) {
|
|||||||
state.shuttingDown = false;
|
state.shuttingDown = false;
|
||||||
state.shutdown = true;
|
state.shutdown = true;
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`);
|
debug(`[${sessionName(self[kType])}] shutdown failed! code: ${ret}`);
|
||||||
const err = new NghttpError(ret);
|
const err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, self, 'error', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
process.nextTick(emit.bind(this, 'shutdown', options));
|
process.nextTick(emit, self, 'shutdown', options);
|
||||||
debug(`[${sessionName(this[kType])}] shutdown is complete`);
|
debug(`[${sessionName(self[kType])}] shutdown is complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit a graceful or immediate shutdown request for the Http2Session.
|
// Submit a graceful or immediate shutdown request for the Http2Session.
|
||||||
@ -659,14 +658,14 @@ function submitShutdown(options) {
|
|||||||
// first send a shutdown notice
|
// first send a shutdown notice
|
||||||
handle.submitShutdownNotice();
|
handle.submitShutdownNotice();
|
||||||
// then, on flip of the event loop, do the actual shutdown
|
// then, on flip of the event loop, do the actual shutdown
|
||||||
setImmediate(doShutdown.bind(this, options));
|
setImmediate(doShutdown, this, options);
|
||||||
} else {
|
} else {
|
||||||
doShutdown.call(this, options);
|
doShutdown(this, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishSessionDestroy(socket) {
|
function finishSessionDestroy(self, socket) {
|
||||||
const state = this[kState];
|
const state = self[kState];
|
||||||
if (state.destroyed)
|
if (state.destroyed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -677,15 +676,15 @@ function finishSessionDestroy(socket) {
|
|||||||
state.destroyed = true;
|
state.destroyed = true;
|
||||||
|
|
||||||
// Destroy the handle
|
// Destroy the handle
|
||||||
const handle = this[kHandle];
|
const handle = self[kHandle];
|
||||||
if (handle !== undefined) {
|
if (handle !== undefined) {
|
||||||
handle.destroy(state.skipUnconsume);
|
handle.destroy(state.skipUnconsume);
|
||||||
debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`);
|
debug(`[${sessionName(self[kType])}] nghttp2session handle destroyed`);
|
||||||
}
|
}
|
||||||
this[kHandle] = undefined;
|
self[kHandle] = undefined;
|
||||||
|
|
||||||
process.nextTick(emit.bind(this, 'close'));
|
process.nextTick(emit, self, 'close');
|
||||||
debug(`[${sessionName(this[kType])}] nghttp2session destroyed`);
|
debug(`[${sessionName(self[kType])}] nghttp2session destroyed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upon creation, the Http2Session takes ownership of the socket. The session
|
// Upon creation, the Http2Session takes ownership of the socket. The session
|
||||||
@ -971,7 +970,7 @@ class Http2Session extends EventEmitter {
|
|||||||
if (this[kHandle] !== undefined)
|
if (this[kHandle] !== undefined)
|
||||||
this[kHandle].destroying();
|
this[kHandle].destroying();
|
||||||
|
|
||||||
setImmediate(finishSessionDestroy.bind(this, socket));
|
setImmediate(finishSessionDestroy, this, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graceful or immediate shutdown of the Http2Session. Graceful shutdown
|
// Graceful or immediate shutdown of the Http2Session. Graceful shutdown
|
||||||
@ -1039,7 +1038,7 @@ class Http2Session extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onTimeout() {
|
_onTimeout() {
|
||||||
process.nextTick(emit.bind(this, 'timeout'));
|
process.nextTick(emit, this, 'timeout');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1168,7 +1167,7 @@ function onHandleFinish() {
|
|||||||
const session = this[kSession];
|
const session = this[kSession];
|
||||||
if (session === undefined) return;
|
if (session === undefined) return;
|
||||||
if (this[kID] === undefined) {
|
if (this[kID] === undefined) {
|
||||||
this.once('ready', onHandleFinish.bind(this));
|
this.once('ready', onHandleFinish);
|
||||||
} else {
|
} else {
|
||||||
const handle = session[kHandle];
|
const handle = session[kHandle];
|
||||||
if (handle !== undefined) {
|
if (handle !== undefined) {
|
||||||
@ -1201,7 +1200,7 @@ function streamOnResume() {
|
|||||||
if (this._paused)
|
if (this._paused)
|
||||||
return this.pause();
|
return this.pause();
|
||||||
if (this[kID] === undefined) {
|
if (this[kID] === undefined) {
|
||||||
this.once('ready', streamOnResume.bind(this));
|
this.once('ready', streamOnResume);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const session = this[kSession];
|
const session = this[kSession];
|
||||||
@ -1238,7 +1237,7 @@ function streamOnSessionConnect() {
|
|||||||
debug(`[${sessionName(session[kType])}] session connected. emiting stream ` +
|
debug(`[${sessionName(session[kType])}] session connected. emiting stream ` +
|
||||||
'connect');
|
'connect');
|
||||||
this[kState].connecting = false;
|
this[kState].connecting = false;
|
||||||
process.nextTick(emit.bind(this, 'connect'));
|
process.nextTick(emit, this, 'connect');
|
||||||
}
|
}
|
||||||
|
|
||||||
function streamOnceReady() {
|
function streamOnceReady() {
|
||||||
@ -1320,7 +1319,7 @@ class Http2Stream extends Duplex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onTimeout() {
|
_onTimeout() {
|
||||||
process.nextTick(emit.bind(this, 'timeout'));
|
process.nextTick(emit, this, 'timeout');
|
||||||
}
|
}
|
||||||
|
|
||||||
// true if the Http2Stream was aborted abornomally.
|
// true if the Http2Stream was aborted abornomally.
|
||||||
@ -1485,7 +1484,6 @@ class Http2Stream extends Duplex {
|
|||||||
// * Then cleans up the resources on the js side
|
// * Then cleans up the resources on the js side
|
||||||
_destroy(err, callback) {
|
_destroy(err, callback) {
|
||||||
const session = this[kSession];
|
const session = this[kSession];
|
||||||
const handle = session[kHandle];
|
|
||||||
if (this[kID] === undefined) {
|
if (this[kID] === undefined) {
|
||||||
debug(`[${sessionName(session[kType])}] queuing destroy for new stream`);
|
debug(`[${sessionName(session[kType])}] queuing destroy for new stream`);
|
||||||
this.once('ready', this._destroy.bind(this, err, callback));
|
this.once('ready', this._destroy.bind(this, err, callback));
|
||||||
@ -1498,47 +1496,53 @@ class Http2Stream extends Duplex {
|
|||||||
server.emit('streamError', err, this);
|
server.emit('streamError', err, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(continueStreamDestroy, this, err, callback);
|
||||||
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueStreamDestroy(self, err, callback) {
|
||||||
|
const session = self[kSession];
|
||||||
|
const handle = session[kHandle];
|
||||||
|
const state = self[kState];
|
||||||
|
|
||||||
|
debug(`[${sessionName(session[kType])}] destroying stream ${self[kID]}`);
|
||||||
|
|
||||||
// Submit RST-STREAM frame if one hasn't been sent already and the
|
// Submit RST-STREAM frame if one hasn't been sent already and the
|
||||||
// stream hasn't closed normally...
|
// stream hasn't closed normally...
|
||||||
if (!this[kState].rst && !session.destroyed) {
|
if (!state.rst && !session.destroyed) {
|
||||||
const code =
|
const code =
|
||||||
err instanceof Error ?
|
err instanceof Error ?
|
||||||
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
|
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
|
||||||
this[kSession].rstStream(this, code);
|
session.rstStream(self, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the close handler on the session
|
// Remove the close handler on the session
|
||||||
session.removeListener('close', this[kState].closeHandler);
|
session.removeListener('close', state.closeHandler);
|
||||||
|
|
||||||
// Unenroll the timer
|
// Unenroll the timer
|
||||||
this.setTimeout(0);
|
self.setTimeout(0);
|
||||||
|
|
||||||
setImmediate(finishStreamDestroy.bind(this, handle));
|
setImmediate(finishStreamDestroy, self, handle);
|
||||||
|
|
||||||
// All done
|
// All done
|
||||||
const rst = this[kState].rst;
|
const rst = state.rst;
|
||||||
const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
|
const code = rst ? state.rstCode : NGHTTP2_NO_ERROR;
|
||||||
if (!err && code !== NGHTTP2_NO_ERROR) {
|
if (!err && code !== NGHTTP2_NO_ERROR) {
|
||||||
err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
|
err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
|
||||||
}
|
}
|
||||||
process.nextTick(emit.bind(this, 'streamClosed', code));
|
process.nextTick(emit, self, 'streamClosed', code);
|
||||||
debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
|
debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`);
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishStreamDestroy(handle) {
|
function finishStreamDestroy(self, handle) {
|
||||||
const id = this[kID];
|
const id = self[kID];
|
||||||
const session = this[kSession];
|
const session = self[kSession];
|
||||||
session[kState].streams.delete(id);
|
session[kState].streams.delete(id);
|
||||||
delete this[kSession];
|
delete self[kSession];
|
||||||
if (handle !== undefined)
|
if (handle !== undefined)
|
||||||
handle.destroyStream(id);
|
handle.destroyStream(id);
|
||||||
this.emit('destroy');
|
self.emit('destroy');
|
||||||
}
|
}
|
||||||
|
|
||||||
function processHeaders(headers) {
|
function processHeaders(headers) {
|
||||||
@ -1578,12 +1582,12 @@ function processRespondWithFD(fd, headers, offset = 0, length = -1,
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1594,7 +1598,7 @@ function doSendFD(session, options, fd, headers, getTrailers, err, stat) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err) {
|
if (err) {
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1611,7 +1615,8 @@ function doSendFD(session, options, fd, headers, getTrailers, err, stat) {
|
|||||||
const headersList = mapToHeaders(headers,
|
const headersList = mapToHeaders(headers,
|
||||||
assertValidPseudoHeaderResponse);
|
assertValidPseudoHeaderResponse);
|
||||||
if (!Array.isArray(headersList)) {
|
if (!Array.isArray(headersList)) {
|
||||||
process.nextTick(() => this.emit('error', headersList));
|
process.nextTick(emit, this, 'error', headersList);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processRespondWithFD.call(this, fd, headersList,
|
processRespondWithFD.call(this, fd, headersList,
|
||||||
@ -1668,7 +1673,8 @@ function doSendFileFD(session, options, fd, headers, getTrailers, err, stat) {
|
|||||||
const headersList = mapToHeaders(headers,
|
const headersList = mapToHeaders(headers,
|
||||||
assertValidPseudoHeaderResponse);
|
assertValidPseudoHeaderResponse);
|
||||||
if (!Array.isArray(headersList)) {
|
if (!Array.isArray(headersList)) {
|
||||||
process.nextTick(() => this.emit('error', headersList));
|
process.nextTick(emit, this, 'error', headersList);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processRespondWithFD.call(this, fd, headersList,
|
processRespondWithFD.call(this, fd, headersList,
|
||||||
@ -1786,20 +1792,20 @@ class ServerHttp2Stream extends Http2Stream {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
||||||
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
case NGHTTP2_ERR_STREAM_CLOSED:
|
case NGHTTP2_ERR_STREAM_CLOSED:
|
||||||
err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
|
err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (ret <= 0) {
|
if (ret <= 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
debug(`[${sessionName(session[kType])}] push stream ${ret} created`);
|
debug(`[${sessionName(session[kType])}] push stream ${ret} created`);
|
||||||
@ -1882,12 +1888,12 @@ class ServerHttp2Stream extends Http2Stream {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => session.emit('error', err));
|
process.nextTick(emit, session, 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1963,7 +1969,8 @@ class ServerHttp2Stream extends Http2Stream {
|
|||||||
const headersList = mapToHeaders(headers,
|
const headersList = mapToHeaders(headers,
|
||||||
assertValidPseudoHeaderResponse);
|
assertValidPseudoHeaderResponse);
|
||||||
if (!Array.isArray(headersList)) {
|
if (!Array.isArray(headersList)) {
|
||||||
process.nextTick(() => this.emit('error', headersList));
|
process.nextTick(emit, this, 'error', headersList);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processRespondWithFD.call(this, fd, headersList,
|
processRespondWithFD.call(this, fd, headersList,
|
||||||
@ -2079,12 +2086,12 @@ class ServerHttp2Stream extends Http2Stream {
|
|||||||
switch (ret) {
|
switch (ret) {
|
||||||
case NGHTTP2_ERR_NOMEM:
|
case NGHTTP2_ERR_NOMEM:
|
||||||
err = new errors.Error('ERR_OUTOFMEMORY');
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
||||||
process.nextTick(() => this[kSession].emit('error', err));
|
process.nextTick(emit, this[kSession], 'error', err);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
err = new NghttpError(ret);
|
err = new NghttpError(ret);
|
||||||
process.nextTick(() => this.emit('error', err));
|
process.nextTick(emit, this, 'error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2289,7 +2296,7 @@ function connectionListener(socket) {
|
|||||||
|
|
||||||
socket[kServer] = this;
|
socket[kServer] = this;
|
||||||
|
|
||||||
process.nextTick(emit.bind(this, 'session', session));
|
process.nextTick(emit, this, 'session', session);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeOptions(options) {
|
function initializeOptions(options) {
|
||||||
|
105
test/parallel/test-http2-info-headers-errors.js
Normal file
105
test/parallel/test-http2-info-headers-errors.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within additionalHeaders
|
||||||
|
// - NGHTTP2_ERR_NOMEM (should emit session error)
|
||||||
|
// - every other NGHTTP2 error from binding (should emit stream error)
|
||||||
|
|
||||||
|
const specificTestKeys = [
|
||||||
|
'NGHTTP2_ERR_NOMEM'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificTests = [
|
||||||
|
{
|
||||||
|
ngError: constants.NGHTTP2_ERR_NOMEM,
|
||||||
|
error: {
|
||||||
|
code: 'ERR_OUTOFMEMORY',
|
||||||
|
type: Error,
|
||||||
|
message: 'Out of memory'
|
||||||
|
},
|
||||||
|
type: 'session'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const genericTests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
},
|
||||||
|
type: 'stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const tests = specificTests.concat(genericTests);
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock sendHeaders because we only care about testing error handling
|
||||||
|
Http2Session.prototype.sendHeaders = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
stream.on('error', errorMustCall);
|
||||||
|
stream.on('error', common.mustCall(() => {
|
||||||
|
stream.respond();
|
||||||
|
stream.end();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.additionalHeaders({ ':status': 100 });
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
@ -32,6 +32,15 @@ function onStream(stream, headers, flags) {
|
|||||||
message: status101regex
|
message: status101regex
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => stream.additionalHeaders({ ':method': 'POST' }),
|
||||||
|
{
|
||||||
|
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||||
|
type: Error,
|
||||||
|
message: '":method" is an invalid pseudoheader or is used incorrectly'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Can send more than one
|
// Can send more than one
|
||||||
stream.additionalHeaders({ ':status': 100 });
|
stream.additionalHeaders({ ':status': 100 });
|
||||||
stream.additionalHeaders({ ':status': 100 });
|
stream.additionalHeaders({ ':status': 100 });
|
||||||
|
109
test/parallel/test-http2-priority-errors.js
Normal file
109
test/parallel/test-http2-priority-errors.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within priority
|
||||||
|
// - NGHTTP2_ERR_NOMEM (should emit session error)
|
||||||
|
// - every other NGHTTP2 error from binding (should emit stream error)
|
||||||
|
|
||||||
|
const specificTestKeys = [
|
||||||
|
'NGHTTP2_ERR_NOMEM'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificTests = [
|
||||||
|
{
|
||||||
|
ngError: constants.NGHTTP2_ERR_NOMEM,
|
||||||
|
error: {
|
||||||
|
code: 'ERR_OUTOFMEMORY',
|
||||||
|
type: Error,
|
||||||
|
message: 'Out of memory'
|
||||||
|
},
|
||||||
|
type: 'session'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const genericTests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
},
|
||||||
|
type: 'stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const tests = specificTests.concat(genericTests);
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock submitPriority because we only care about testing error handling
|
||||||
|
Http2Session.prototype.submitPriority = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
stream.on('error', errorMustCall);
|
||||||
|
stream.on('error', common.mustCall(() => {
|
||||||
|
stream.respond();
|
||||||
|
stream.end();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.priority({
|
||||||
|
parent: 0,
|
||||||
|
weight: 1,
|
||||||
|
exclusive: false
|
||||||
|
});
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
104
test/parallel/test-http2-respond-errors.js
Normal file
104
test/parallel/test-http2-respond-errors.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within respond
|
||||||
|
// - NGHTTP2_ERR_NOMEM (should emit session error)
|
||||||
|
// - every other NGHTTP2 error from binding (should emit stream error)
|
||||||
|
|
||||||
|
const specificTestKeys = [
|
||||||
|
'NGHTTP2_ERR_NOMEM'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificTests = [
|
||||||
|
{
|
||||||
|
ngError: constants.NGHTTP2_ERR_NOMEM,
|
||||||
|
error: {
|
||||||
|
code: 'ERR_OUTOFMEMORY',
|
||||||
|
type: Error,
|
||||||
|
message: 'Out of memory'
|
||||||
|
},
|
||||||
|
type: 'session'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const genericTests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
},
|
||||||
|
type: 'stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const tests = specificTests.concat(genericTests);
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock submitResponse because we only care about testing error handling
|
||||||
|
Http2Session.prototype.submitResponse = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
stream.on('error', errorMustCall);
|
||||||
|
stream.on('error', common.mustCall(() => {
|
||||||
|
stream.destroy();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.respond();
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
@ -6,6 +6,11 @@ if (!common.hasCrypto)
|
|||||||
const http2 = require('http2');
|
const http2 = require('http2');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
const {
|
||||||
|
HTTP2_HEADER_CONTENT_TYPE,
|
||||||
|
HTTP2_HEADER_METHOD
|
||||||
|
} = http2.constants;
|
||||||
|
|
||||||
const optionsWithTypeError = {
|
const optionsWithTypeError = {
|
||||||
offset: 'number',
|
offset: 'number',
|
||||||
length: 'number',
|
length: 'number',
|
||||||
@ -54,7 +59,7 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
// Should throw if :status 204, 205 or 304
|
// Should throw if :status 204, 205 or 304
|
||||||
[204, 205, 304].forEach((status) => common.expectsError(
|
[204, 205, 304].forEach((status) => common.expectsError(
|
||||||
() => stream.respondWithFile(fname, {
|
() => stream.respondWithFile(fname, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
|
||||||
':status': status,
|
':status': status,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -63,13 +68,31 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// should emit an error on the stream if headers aren't valid
|
||||||
|
stream.respondWithFile(fname, {
|
||||||
|
[HTTP2_HEADER_METHOD]: 'POST'
|
||||||
|
}, {
|
||||||
|
statCheck: common.mustCall(() => {
|
||||||
|
// give time to the current test case to finish
|
||||||
|
process.nextTick(continueTest, stream);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
stream.once('error', common.expectsError({
|
||||||
|
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||||
|
type: Error,
|
||||||
|
message: '":method" is an invalid pseudoheader or is used incorrectly'
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
function continueTest(stream) {
|
||||||
// Should throw if headers already sent
|
// Should throw if headers already sent
|
||||||
stream.respond({
|
stream.respond({
|
||||||
':status': 200,
|
':status': 200,
|
||||||
});
|
});
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFile(fname, {
|
() => stream.respondWithFile(fname, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
code: 'ERR_HTTP2_HEADERS_SENT',
|
code: 'ERR_HTTP2_HEADERS_SENT',
|
||||||
@ -81,14 +104,14 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
stream.destroy();
|
stream.destroy();
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFile(fname, {
|
() => stream.respondWithFile(fname, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
code: 'ERR_HTTP2_INVALID_STREAM',
|
code: 'ERR_HTTP2_INVALID_STREAM',
|
||||||
message: 'The stream has been destroyed'
|
message: 'The stream has been destroyed'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}));
|
}
|
||||||
|
|
||||||
server.listen(0, common.mustCall(() => {
|
server.listen(0, common.mustCall(() => {
|
||||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||||
|
@ -7,6 +7,11 @@ const http2 = require('http2');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const {
|
||||||
|
HTTP2_HEADER_CONTENT_TYPE,
|
||||||
|
HTTP2_HEADER_METHOD
|
||||||
|
} = http2.constants;
|
||||||
|
|
||||||
const optionsWithTypeError = {
|
const optionsWithTypeError = {
|
||||||
offset: 'number',
|
offset: 'number',
|
||||||
length: 'number',
|
length: 'number',
|
||||||
@ -38,7 +43,7 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
|
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFD(types[type], {
|
() => stream.respondWithFD(types[type], {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
type: TypeError,
|
type: TypeError,
|
||||||
@ -57,7 +62,7 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
|
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFD(fd, {
|
() => stream.respondWithFD(fd, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}, {
|
}, {
|
||||||
[option]: types[type]
|
[option]: types[type]
|
||||||
}),
|
}),
|
||||||
@ -74,25 +79,49 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
// Should throw if :status 204, 205 or 304
|
// Should throw if :status 204, 205 or 304
|
||||||
[204, 205, 304].forEach((status) => common.expectsError(
|
[204, 205, 304].forEach((status) => common.expectsError(
|
||||||
() => stream.respondWithFD(fd, {
|
() => stream.respondWithFD(fd, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
|
||||||
':status': status,
|
':status': status,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN',
|
code: 'ERR_HTTP2_PAYLOAD_FORBIDDEN',
|
||||||
|
type: Error,
|
||||||
message: `Responses with ${status} status must not have a payload`
|
message: `Responses with ${status} status must not have a payload`
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// should emit an error on the stream if headers aren't valid
|
||||||
|
stream.respondWithFD(fd, {
|
||||||
|
[HTTP2_HEADER_METHOD]: 'POST'
|
||||||
|
}, {
|
||||||
|
statCheck() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.once('error', common.expectsError({
|
||||||
|
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||||
|
type: Error,
|
||||||
|
message: '":method" is an invalid pseudoheader or is used incorrectly'
|
||||||
|
}));
|
||||||
|
stream.respondWithFD(fd, {
|
||||||
|
[HTTP2_HEADER_METHOD]: 'POST'
|
||||||
|
});
|
||||||
|
stream.once('error', common.expectsError({
|
||||||
|
code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
|
||||||
|
type: Error,
|
||||||
|
message: '":method" is an invalid pseudoheader or is used incorrectly'
|
||||||
|
}));
|
||||||
|
|
||||||
// Should throw if headers already sent
|
// Should throw if headers already sent
|
||||||
stream.respond({
|
stream.respond({
|
||||||
':status': 200,
|
':status': 200,
|
||||||
});
|
});
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFD(fd, {
|
() => stream.respondWithFD(fd, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
code: 'ERR_HTTP2_HEADERS_SENT',
|
code: 'ERR_HTTP2_HEADERS_SENT',
|
||||||
|
type: Error,
|
||||||
message: 'Response has already been initiated.'
|
message: 'Response has already been initiated.'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -101,10 +130,11 @@ server.on('stream', common.mustCall((stream) => {
|
|||||||
stream.destroy();
|
stream.destroy();
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => stream.respondWithFD(fd, {
|
() => stream.respondWithFD(fd, {
|
||||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
[HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
code: 'ERR_HTTP2_INVALID_STREAM',
|
code: 'ERR_HTTP2_INVALID_STREAM',
|
||||||
|
type: Error,
|
||||||
message: 'The stream has been destroyed'
|
message: 'The stream has been destroyed'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
109
test/parallel/test-http2-respond-with-fd-errors.js
Normal file
109
test/parallel/test-http2-respond-with-fd-errors.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within processRespondWithFD
|
||||||
|
// (called by respondWithFD & respondWithFile)
|
||||||
|
// - NGHTTP2_ERR_NOMEM (should emit session error)
|
||||||
|
// - every other NGHTTP2 error from binding (should emit stream error)
|
||||||
|
|
||||||
|
const fname = path.resolve(common.fixturesDir, 'elipses.txt');
|
||||||
|
|
||||||
|
const specificTestKeys = [
|
||||||
|
'NGHTTP2_ERR_NOMEM'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificTests = [
|
||||||
|
{
|
||||||
|
ngError: constants.NGHTTP2_ERR_NOMEM,
|
||||||
|
error: {
|
||||||
|
code: 'ERR_OUTOFMEMORY',
|
||||||
|
type: Error,
|
||||||
|
message: 'Out of memory'
|
||||||
|
},
|
||||||
|
type: 'session'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const genericTests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
},
|
||||||
|
type: 'stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const tests = specificTests.concat(genericTests);
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock submitFile because we only care about testing error handling
|
||||||
|
Http2Session.prototype.submitFile = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
stream.on('error', errorMustCall);
|
||||||
|
stream.on('error', common.mustCall(() => {
|
||||||
|
stream.destroy();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.respondWithFile(fname);
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
108
test/parallel/test-http2-rststream-errors.js
Normal file
108
test/parallel/test-http2-rststream-errors.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within rstStream
|
||||||
|
// - NGHTTP2_ERR_NOMEM (should emit session error)
|
||||||
|
// - every other NGHTTP2 error from binding (should emit stream error)
|
||||||
|
|
||||||
|
const specificTestKeys = [
|
||||||
|
'NGHTTP2_ERR_NOMEM'
|
||||||
|
];
|
||||||
|
|
||||||
|
const specificTests = [
|
||||||
|
{
|
||||||
|
ngError: constants.NGHTTP2_ERR_NOMEM,
|
||||||
|
error: {
|
||||||
|
code: 'ERR_OUTOFMEMORY',
|
||||||
|
type: Error,
|
||||||
|
message: 'Out of memory'
|
||||||
|
},
|
||||||
|
type: 'session'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const genericTests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
},
|
||||||
|
type: 'stream'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const tests = specificTests.concat(genericTests);
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock submitRstStream because we only care about testing error handling
|
||||||
|
Http2Session.prototype.submitRstStream = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
stream.on('error', errorMustCall);
|
||||||
|
stream.on('error', common.mustCall(() => {
|
||||||
|
stream.session.destroy();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.rstStream();
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
if (currentError.type === 'stream') {
|
||||||
|
req.on('error', common.mustCall());
|
||||||
|
}
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
@ -81,7 +81,6 @@ server.on('stream', common.mustCall((stream, headers) => {
|
|||||||
const errorMustNotCall = common.mustNotCall(
|
const errorMustNotCall = common.mustNotCall(
|
||||||
`${currentError.error.code} should emit on ${currentError.type}`
|
`${currentError.error.code} should emit on ${currentError.type}`
|
||||||
);
|
);
|
||||||
console.log(currentError);
|
|
||||||
|
|
||||||
if (currentError.type === 'stream') {
|
if (currentError.type === 'stream') {
|
||||||
stream.session.on('error', errorMustNotCall);
|
stream.session.on('error', errorMustNotCall);
|
||||||
|
75
test/parallel/test-http2-shutdown-errors.js
Normal file
75
test/parallel/test-http2-shutdown-errors.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
const http2 = require('http2');
|
||||||
|
const {
|
||||||
|
constants,
|
||||||
|
Http2Session,
|
||||||
|
nghttp2ErrorString
|
||||||
|
} = process.binding('http2');
|
||||||
|
|
||||||
|
// tests error handling within shutdown
|
||||||
|
// - should emit ERR_HTTP2_ERROR on session for all errors
|
||||||
|
|
||||||
|
const tests = Object.getOwnPropertyNames(constants)
|
||||||
|
.filter((key) => (
|
||||||
|
key.indexOf('NGHTTP2_ERR') === 0
|
||||||
|
))
|
||||||
|
.map((key) => ({
|
||||||
|
ngError: constants[key],
|
||||||
|
error: {
|
||||||
|
code: 'ERR_HTTP2_ERROR',
|
||||||
|
type: Error,
|
||||||
|
message: nghttp2ErrorString(constants[key])
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let currentError;
|
||||||
|
|
||||||
|
// mock submitGoaway because we only care about testing error handling
|
||||||
|
Http2Session.prototype.submitGoaway = () => currentError.ngError;
|
||||||
|
|
||||||
|
const server = http2.createServer();
|
||||||
|
server.on('stream', common.mustCall((stream, headers) => {
|
||||||
|
const errorMustCall = common.expectsError(currentError.error);
|
||||||
|
const errorMustNotCall = common.mustNotCall(
|
||||||
|
`${currentError.error.code} should emit on session`
|
||||||
|
);
|
||||||
|
|
||||||
|
stream.session.once('error', errorMustCall);
|
||||||
|
stream.on('error', errorMustNotCall);
|
||||||
|
|
||||||
|
stream.session.shutdown();
|
||||||
|
}, tests.length));
|
||||||
|
|
||||||
|
server.listen(0, common.mustCall(() => runTest(tests.shift())));
|
||||||
|
|
||||||
|
function runTest(test) {
|
||||||
|
const port = server.address().port;
|
||||||
|
const url = `http://localhost:${port}`;
|
||||||
|
const headers = {
|
||||||
|
':path': '/',
|
||||||
|
':method': 'POST',
|
||||||
|
':scheme': 'http',
|
||||||
|
':authority': `localhost:${port}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = http2.connect(url);
|
||||||
|
const req = client.request(headers);
|
||||||
|
|
||||||
|
currentError = test;
|
||||||
|
req.resume();
|
||||||
|
req.end();
|
||||||
|
|
||||||
|
req.on('end', common.mustCall(() => {
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (!tests.length) {
|
||||||
|
server.close();
|
||||||
|
} else {
|
||||||
|
runTest(tests.shift());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user