Skip to content

Commit c3ac4c8

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 5ad3efb commit c3ac4c8

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
@@ -1388,6 +1388,17 @@ class Http2Session extends EventEmitter {
13881388
this[kMaybeDestroy]();
13891389
}
13901390

1391+
[EventEmitter.captureRejectionSymbol](err, event, ...args) {
1392+
switch (event) {
1393+
case 'stream':
1394+
const [stream] = args;
1395+
stream.destroy(err);
1396+
break;
1397+
default:
1398+
this.destroy(err);
1399+
}
1400+
}
1401+
13911402
// Destroy the session if:
13921403
// * error is not undefined/null
13931404
// * session is closed and there are no more pending or open streams
@@ -2875,6 +2886,40 @@ class Http2Server extends NETServer {
28752886
}
28762887
}
28772888

2889+
Http2Server.prototype[EventEmitter.captureRejectionSymbol] = function(
2890+
err, event, ...args) {
2891+
2892+
switch (event) {
2893+
case 'stream':
2894+
// TODO(mcollina): we might want to match this with what we do on
2895+
// the compat side.
2896+
const [stream] = args;
2897+
if (stream.sentHeaders) {
2898+
stream.destroy(err);
2899+
} else {
2900+
stream.respond({ [HTTP2_HEADER_STATUS]: 500 });
2901+
stream.end();
2902+
}
2903+
break;
2904+
case 'request':
2905+
const [, res] = args;
2906+
if (!res.headersSent && !res.finished) {
2907+
// Don't leak headers.
2908+
for (const name of res.getHeaderNames()) {
2909+
res.removeHeader(name);
2910+
}
2911+
res.statusCode = 500;
2912+
res.end(http.STATUS_CODES[500]);
2913+
} else {
2914+
res.destroy();
2915+
}
2916+
break;
2917+
default:
2918+
net.Server.prototype[EventEmitter.captureRejectionSymbol]
2919+
.call(this, err, event, ...args);
2920+
}
2921+
};
2922+
28782923
function setupCompat(ev) {
28792924
if (ev === 'request') {
28802925
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)