Skip to content

Commit 2cc0482

Browse files
mcollinatargos
authored andcommitted
http2: implement capture rection for 'request' and 'stream' events
PR-URL: #27867 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 48fcd76 commit 2cc0482

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

lib/internal/http2/core.js

+45
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,17 @@ class Http2Session extends EventEmitter {
14141414
this[kMaybeDestroy]();
14151415
}
14161416

1417+
[EventEmitter.captureRejectionSymbol](err, event, ...args) {
1418+
switch (event) {
1419+
case 'stream':
1420+
const [stream] = args;
1421+
stream.destroy(err);
1422+
break;
1423+
default:
1424+
this.destroy(err);
1425+
}
1426+
}
1427+
14171428
// Destroy the session if:
14181429
// * error is not undefined/null
14191430
// * session is closed and there are no more pending or open streams
@@ -2905,6 +2916,40 @@ class Http2Server extends NETServer {
29052916
}
29062917
}
29072918

2919+
Http2Server.prototype[EventEmitter.captureRejectionSymbol] = function(
2920+
err, event, ...args) {
2921+
2922+
switch (event) {
2923+
case 'stream':
2924+
// TODO(mcollina): we might want to match this with what we do on
2925+
// the compat side.
2926+
const [stream] = args;
2927+
if (stream.sentHeaders) {
2928+
stream.destroy(err);
2929+
} else {
2930+
stream.respond({ [HTTP2_HEADER_STATUS]: 500 });
2931+
stream.end();
2932+
}
2933+
break;
2934+
case 'request':
2935+
const [, res] = args;
2936+
if (!res.headersSent && !res.finished) {
2937+
// Don't leak headers.
2938+
for (const name of res.getHeaderNames()) {
2939+
res.removeHeader(name);
2940+
}
2941+
res.statusCode = 500;
2942+
res.end(http.STATUS_CODES[500]);
2943+
} else {
2944+
res.destroy();
2945+
}
2946+
break;
2947+
default:
2948+
net.Server.prototype[EventEmitter.captureRejectionSymbol]
2949+
.call(this, err, event, ...args);
2950+
}
2951+
};
2952+
29082953
function setupCompat(ev) {
29092954
if (ev === 'request') {
29102955
this.removeListener('newListener', setupCompat);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const events = require('events');
9+
const { createServer, connect } = require('http2');
10+
11+
events.captureRejections = true;
12+
13+
{
14+
// Test error thrown in the server 'stream' event,
15+
// after a respond()
16+
17+
const server = createServer();
18+
server.on('stream', common.mustCall(async (stream) => {
19+
server.close();
20+
21+
stream.respond({ ':status': 200 });
22+
23+
const _err = new Error('kaboom');
24+
stream.on('error', common.mustCall((err) => {
25+
assert.strictEqual(err, _err);
26+
}));
27+
throw _err;
28+
}));
29+
30+
server.listen(0, common.mustCall(() => {
31+
const { port } = server.address();
32+
const session = connect(`http://localhost:${port}`);
33+
34+
const req = session.request();
35+
36+
req.on('error', common.mustCall((err) => {
37+
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
38+
}));
39+
40+
req.on('close', common.mustCall(() => {
41+
session.close();
42+
}));
43+
}));
44+
}
45+
46+
{
47+
// Test error thrown in the server 'stream' event,
48+
// before a respond().
49+
50+
const server = createServer();
51+
server.on('stream', common.mustCall(async (stream) => {
52+
server.close();
53+
54+
stream.on('error', common.mustNotCall());
55+
56+
throw new Error('kaboom');
57+
}));
58+
59+
server.listen(0, common.mustCall(() => {
60+
const { port } = server.address();
61+
const session = connect(`http://localhost:${port}`);
62+
63+
const req = session.request();
64+
65+
req.on('response', common.mustCall((headers) => {
66+
assert.strictEqual(headers[':status'], 500);
67+
}));
68+
69+
req.on('close', common.mustCall(() => {
70+
session.close();
71+
}));
72+
}));
73+
}
74+
75+
76+
{
77+
// Test error thrown in 'request' event
78+
79+
const server = createServer(common.mustCall(async (req, res) => {
80+
server.close();
81+
res.setHeader('content-type', 'application/json');
82+
const _err = new Error('kaboom');
83+
throw _err;
84+
}));
85+
86+
server.listen(0, common.mustCall(() => {
87+
const { port } = server.address();
88+
const session = connect(`http://localhost:${port}`);
89+
90+
const req = session.request();
91+
92+
req.on('response', common.mustCall((headers) => {
93+
assert.strictEqual(headers[':status'], 500);
94+
assert.strictEqual(Object.hasOwnProperty.call(headers, 'content-type'),
95+
false);
96+
}));
97+
98+
req.on('close', common.mustCall(() => {
99+
session.close();
100+
}));
101+
102+
req.resume();
103+
}));
104+
}
105+
106+
{
107+
// Test error thrown in the client 'stream' event
108+
109+
const server = createServer();
110+
server.on('stream', common.mustCall(async (stream) => {
111+
const { port } = server.address();
112+
113+
server.close();
114+
115+
stream.pushStream({
116+
':scheme': 'http',
117+
':path': '/foobar',
118+
':authority': `localhost:${port}`,
119+
}, common.mustCall((err, push) => {
120+
push.respond({
121+
'content-type': 'text/html',
122+
':status': 200
123+
});
124+
push.end('pushed by the server');
125+
126+
stream.end('test');
127+
}));
128+
129+
stream.respond({
130+
':status': 200
131+
});
132+
}));
133+
134+
server.listen(0, common.mustCall(() => {
135+
const { port } = server.address();
136+
const session = connect(`http://localhost:${port}`);
137+
138+
const req = session.request();
139+
140+
session.on('stream', common.mustCall(async (stream) => {
141+
session.close();
142+
143+
const _err = new Error('kaboom');
144+
stream.on('error', common.mustCall((err) => {
145+
assert.strictEqual(err, _err);
146+
}));
147+
throw _err;
148+
}));
149+
150+
req.end();
151+
}));
152+
}

0 commit comments

Comments
 (0)