From d7d7cf7513579910d8faa467cb678df6fa90700f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 2 Oct 2019 13:02:37 +0200 Subject: [PATCH] http2: implement capture rection for 'request' and 'stream' events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/27867 Reviewed-By: Benjamin Gruenbaum Reviewed-By: James M Snell Reviewed-By: Jeremiah Senkpiel Reviewed-By: Anna Henningsen Reviewed-By: Michaƫl Zasso --- lib/internal/http2/core.js | 45 ++++++ test/parallel/test-http2-capture-rejection.js | 152 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 test/parallel/test-http2-capture-rejection.js diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 4e40460f744d30..807f143421173e 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1414,6 +1414,17 @@ class Http2Session extends EventEmitter { this[kMaybeDestroy](); } + [EventEmitter.captureRejectionSymbol](err, event, ...args) { + switch (event) { + case 'stream': + const [stream] = args; + 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 @@ -2905,6 +2916,40 @@ class Http2Server extends NETServer { } } +Http2Server.prototype[EventEmitter.captureRejectionSymbol] = function( + err, event, ...args) { + + switch (event) { + case 'stream': + // TODO(mcollina): we might want to match this with what we do on + // the compat side. + const [stream] = args; + if (stream.sentHeaders) { + stream.destroy(err); + } else { + stream.respond({ [HTTP2_HEADER_STATUS]: 500 }); + stream.end(); + } + break; + case 'request': + const [, res] = args; + if (!res.headersSent && !res.finished) { + // Don't leak headers. + for (const name of res.getHeaderNames()) { + res.removeHeader(name); + } + res.statusCode = 500; + res.end(http.STATUS_CODES[500]); + } else { + res.destroy(); + } + break; + default: + net.Server.prototype[EventEmitter.captureRejectionSymbol] + .call(this, err, event, ...args); + } +}; + function setupCompat(ev) { if (ev === 'request') { this.removeListener('newListener', setupCompat); diff --git a/test/parallel/test-http2-capture-rejection.js b/test/parallel/test-http2-capture-rejection.js new file mode 100644 index 00000000000000..58f43581eb6bd3 --- /dev/null +++ b/test/parallel/test-http2-capture-rejection.js @@ -0,0 +1,152 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const events = require('events'); +const { createServer, connect } = require('http2'); + +events.captureRejections = true; + +{ + // Test error thrown in the server 'stream' event, + // after a respond() + + const server = createServer(); + server.on('stream', common.mustCall(async (stream) => { + server.close(); + + stream.respond({ ':status': 200 }); + + const _err = new Error('kaboom'); + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + throw _err; + })); + + server.listen(0, common.mustCall(() => { + const { port } = server.address(); + const session = connect(`http://localhost:${port}`); + + const req = session.request(); + + req.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); + })); + + req.on('close', common.mustCall(() => { + session.close(); + })); + })); +} + +{ + // Test error thrown in the server 'stream' event, + // before a respond(). + + const server = createServer(); + server.on('stream', common.mustCall(async (stream) => { + server.close(); + + stream.on('error', common.mustNotCall()); + + throw new Error('kaboom'); + })); + + server.listen(0, common.mustCall(() => { + const { port } = server.address(); + const session = connect(`http://localhost:${port}`); + + const req = session.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 500); + })); + + req.on('close', common.mustCall(() => { + session.close(); + })); + })); +} + + +{ + // Test error thrown in 'request' event + + const server = createServer(common.mustCall(async (req, res) => { + server.close(); + res.setHeader('content-type', 'application/json'); + const _err = new Error('kaboom'); + throw _err; + })); + + server.listen(0, common.mustCall(() => { + const { port } = server.address(); + const session = connect(`http://localhost:${port}`); + + const req = session.request(); + + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 500); + assert.strictEqual(Object.hasOwnProperty.call(headers, 'content-type'), + false); + })); + + req.on('close', common.mustCall(() => { + session.close(); + })); + + req.resume(); + })); +} + +{ + // Test error thrown in the client 'stream' event + + const server = createServer(); + server.on('stream', common.mustCall(async (stream) => { + const { port } = server.address(); + + server.close(); + + stream.pushStream({ + ':scheme': 'http', + ':path': '/foobar', + ':authority': `localhost:${port}`, + }, common.mustCall((err, push) => { + push.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + push.end('pushed by the server'); + + stream.end('test'); + })); + + stream.respond({ + ':status': 200 + }); + })); + + server.listen(0, common.mustCall(() => { + const { port } = server.address(); + const session = connect(`http://localhost:${port}`); + + const req = session.request(); + + session.on('stream', common.mustCall(async (stream) => { + session.close(); + + const _err = new Error('kaboom'); + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err, _err); + })); + throw _err; + })); + + req.end(); + })); +}