Skip to content

Commit bc23681

Browse files
apapirovskimcollina
authored andcommitted
http2: small fixes to compatibility layer
Expand argument validation through compat API, adjust behaviour of response.end to not throw if stream already closed to match http1, adjust behaviour of writeContinue to not throw if stream already closed and other very small tweaks. Add tests for added and fixed behaviour. Add tests for edge case behaviours of setTimeout, createPushResponse, destroy, end and trailers. PR-URL: #15473 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Minwoo Jung <[email protected]>
1 parent e5c290b commit bc23681

10 files changed

+200
-33
lines changed

lib/internal/http2/compat.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function onStreamError(error) {
9292
// errors in compatibility mode are
9393
// not forwarded to the request
9494
// and response objects. However,
95-
// they are forwarded to 'clientError'
95+
// they are forwarded to 'streamError'
9696
// on the server by Http2Stream
9797
}
9898

@@ -248,9 +248,9 @@ class Http2ServerRequest extends Readable {
248248
}
249249

250250
setTimeout(msecs, callback) {
251-
const stream = this[kStream];
252-
if (stream === undefined) return;
253-
stream.setTimeout(msecs, callback);
251+
if (this[kState].closed)
252+
return;
253+
this[kStream].setTimeout(msecs, callback);
254254
}
255255

256256
[kFinish](code) {
@@ -445,7 +445,7 @@ class Http2ServerResponse extends Stream {
445445

446446
if (stream === undefined) {
447447
const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
448-
if (cb)
448+
if (typeof cb === 'function')
449449
process.nextTick(cb, err);
450450
else
451451
throw err;
@@ -461,12 +461,11 @@ class Http2ServerResponse extends Stream {
461461
if (typeof chunk === 'function') {
462462
cb = chunk;
463463
chunk = null;
464-
encoding = 'utf8';
465464
} else if (typeof encoding === 'function') {
466465
cb = encoding;
467466
encoding = 'utf8';
468467
}
469-
if (stream === undefined || stream.finished === true) {
468+
if (this.finished === true) {
470469
return false;
471470
}
472471
if (chunk !== null && chunk !== undefined) {
@@ -482,21 +481,21 @@ class Http2ServerResponse extends Stream {
482481
}
483482

484483
destroy(err) {
485-
const stream = this[kStream];
486-
if (stream === undefined) {
487-
// nothing to do, already closed
484+
if (this[kState].closed)
488485
return;
489-
}
490-
stream.destroy(err);
486+
this[kStream].destroy(err);
491487
}
492488

493489
setTimeout(msecs, callback) {
494490
const stream = this[kStream];
495-
if (stream === undefined) return;
491+
if (this[kState].closed)
492+
return;
496493
stream.setTimeout(msecs, callback);
497494
}
498495

499496
createPushResponse(headers, callback) {
497+
if (typeof callback !== 'function')
498+
throw new errors.TypeError('ERR_INVALID_CALLBACK');
500499
const stream = this[kStream];
501500
if (stream === undefined) {
502501
process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
@@ -513,12 +512,9 @@ class Http2ServerResponse extends Stream {
513512
if (stream !== undefined &&
514513
stream.destroyed === false &&
515514
stream.headersSent === false) {
516-
options = options || Object.create(null);
517-
const state = this[kState];
518515
const headers = this[kHeaders];
519-
headers[HTTP2_HEADER_STATUS] = state.statusCode;
520-
if (stream.finished === true)
521-
options.endStream = true;
516+
headers[HTTP2_HEADER_STATUS] = this[kState].statusCode;
517+
options = options || Object.create(null);
522518
options.getTrailers = (trailers) => {
523519
Object.assign(trailers, this[kTrailers]);
524520
};
@@ -542,7 +538,11 @@ class Http2ServerResponse extends Stream {
542538
// TODO doesn't support callbacks
543539
writeContinue() {
544540
const stream = this[kStream];
545-
if (stream === undefined) return false;
541+
if (stream === undefined ||
542+
stream.headersSent === true ||
543+
stream.destroyed === true) {
544+
return false;
545+
}
546546
this[kStream].additionalHeaders({
547547
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE
548548
});

test/parallel/test-http2-compat-expect-continue-check.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ function handler(req, res) {
2121
'abcd': '1'
2222
});
2323
res.end(testResBody);
24+
// should simply return false if already too late to write
25+
assert.strictEqual(res.writeContinue(), false);
26+
res.on('finish', common.mustCall(
27+
() => process.nextTick(() => assert.strictEqual(res.writeContinue(), false))
28+
));
2429
}
2530

2631
const server = http2.createServer(

test/parallel/test-http2-compat-serverrequest-settimeout.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
const common = require('../common');
55
if (!common.hasCrypto)
66
common.skip('missing crypto');
7+
const assert = require('assert');
78
const http2 = require('http2');
89

10+
const msecs = common.platformTimeout(1);
911
const server = http2.createServer();
1012

1113
server.on('request', (req, res) => {
12-
req.setTimeout(common.platformTimeout(1), common.mustCall(() => {
14+
req.setTimeout(msecs, common.mustCall(() => {
1315
res.end();
16+
req.setTimeout(msecs, common.mustNotCall());
17+
}));
18+
res.on('finish', common.mustCall(() => {
19+
req.setTimeout(msecs, common.mustNotCall());
20+
process.nextTick(() => {
21+
assert.doesNotThrow(
22+
() => req.setTimeout(msecs, common.mustNotCall())
23+
);
24+
server.close();
25+
});
1426
}));
1527
});
1628

@@ -24,7 +36,6 @@ server.listen(0, common.mustCall(() => {
2436
':authority': `localhost:${port}`
2537
});
2638
req.on('end', common.mustCall(() => {
27-
server.close();
2839
client.destroy();
2940
}));
3041
req.resume();

test/parallel/test-http2-compat-serverrequest.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ server.listen(0, common.mustCall(function() {
3434
response.on('finish', common.mustCall(function() {
3535
assert.strictEqual(request.closed, true);
3636
assert.strictEqual(request.code, h2.constants.NGHTTP2_NO_ERROR);
37-
server.close();
37+
process.nextTick(() => {
38+
assert.strictEqual(request.socket, undefined);
39+
server.close();
40+
});
3841
}));
3942
response.end();
4043
}));

test/parallel/test-http2-compat-serverresponse-createpushresponse.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ const server = h2.createServer((request, response) => {
1616
assert.strictEqual(response.stream.id % 2, 1);
1717
response.write(servExpect);
1818

19+
// callback must be specified (and be a function)
20+
common.expectsError(
21+
() => response.createPushResponse({
22+
':path': '/pushed',
23+
':method': 'GET'
24+
}, undefined),
25+
{
26+
code: 'ERR_INVALID_CALLBACK',
27+
type: TypeError,
28+
message: 'Callback must be a function'
29+
}
30+
);
31+
1932
response.createPushResponse({
2033
':path': '/pushed',
2134
':method': 'GET'

test/parallel/test-http2-compat-serverresponse-destroy.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const server = http2.createServer(common.mustCall((req, res) => {
2424
res.on('finish', common.mustCall(() => {
2525
assert.doesNotThrow(() => res.destroy(nextError));
2626
assert.strictEqual(res.closed, true);
27+
process.nextTick(() => {
28+
assert.doesNotThrow(() => res.destroy(nextError));
29+
});
2730
}));
2831

2932
if (req.url !== '/') {

test/parallel/test-http2-compat-serverresponse-end.js

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
const { mustCall, mustNotCall, hasCrypto, skip } = require('../common');
55
if (!hasCrypto)
66
skip('missing crypto');
7-
const { doesNotThrow, strictEqual } = require('assert');
7+
const { strictEqual } = require('assert');
88
const {
99
createServer,
1010
connect,
@@ -15,18 +15,82 @@ const {
1515
} = require('http2');
1616

1717
{
18-
// Http2ServerResponse.end callback is called only the first time,
19-
// but may be invoked repeatedly without throwing errors.
18+
// Http2ServerResponse.end accepts chunk, encoding, cb as args
19+
// It may be invoked repeatedly without throwing errors
20+
// but callback will only be called once
2021
const server = createServer(mustCall((request, response) => {
2122
strictEqual(response.closed, false);
22-
response.on('finish', mustCall(() => process.nextTick(
23-
mustCall(() => doesNotThrow(() => response.end('test', mustNotCall())))
24-
)));
23+
response.end('end', 'utf8', mustCall(() => {
24+
strictEqual(response.closed, true);
25+
response.end(mustNotCall());
26+
process.nextTick(() => {
27+
response.end(mustNotCall());
28+
server.close();
29+
});
30+
}));
31+
response.end(mustNotCall());
32+
}));
33+
server.listen(0, mustCall(() => {
34+
let data = '';
35+
const { port } = server.address();
36+
const url = `http://localhost:${port}`;
37+
const client = connect(url, mustCall(() => {
38+
const headers = {
39+
':path': '/',
40+
':method': 'GET',
41+
':scheme': 'http',
42+
':authority': `localhost:${port}`
43+
};
44+
const request = client.request(headers);
45+
request.setEncoding('utf8');
46+
request.on('data', (chunk) => (data += chunk));
47+
request.on('end', mustCall(() => {
48+
strictEqual(data, 'end');
49+
client.destroy();
50+
}));
51+
request.end();
52+
request.resume();
53+
}));
54+
}));
55+
}
56+
57+
{
58+
// Http2ServerResponse.end can omit encoding arg, sets it to utf-8
59+
const server = createServer(mustCall((request, response) => {
60+
response.end('test\uD83D\uDE00', mustCall(() => {
61+
server.close();
62+
}));
63+
}));
64+
server.listen(0, mustCall(() => {
65+
let data = '';
66+
const { port } = server.address();
67+
const url = `http://localhost:${port}`;
68+
const client = connect(url, mustCall(() => {
69+
const headers = {
70+
':path': '/',
71+
':method': 'GET',
72+
':scheme': 'http',
73+
':authority': `localhost:${port}`
74+
};
75+
const request = client.request(headers);
76+
request.setEncoding('utf8');
77+
request.on('data', (chunk) => (data += chunk));
78+
request.on('end', mustCall(() => {
79+
strictEqual(data, 'test\uD83D\uDE00');
80+
client.destroy();
81+
}));
82+
request.end();
83+
request.resume();
84+
}));
85+
}));
86+
}
87+
88+
{
89+
// Http2ServerResponse.end can omit chunk & encoding args
90+
const server = createServer(mustCall((request, response) => {
2591
response.end(mustCall(() => {
2692
server.close();
2793
}));
28-
response.end(mustNotCall());
29-
strictEqual(response.closed, true);
3094
}));
3195
server.listen(0, mustCall(() => {
3296
const { port } = server.address();

test/parallel/test-http2-compat-serverresponse-headers.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ server.listen(0, common.mustCall(function() {
103103
message: 'The "name" argument must be of type string'
104104
}
105105
);
106+
common.expectsError(
107+
() => response.setHeader(''),
108+
{
109+
code: 'ERR_INVALID_HTTP_TOKEN',
110+
type: TypeError,
111+
message: 'Header name must be a valid HTTP token [""]'
112+
}
113+
);
106114

107115
response.setHeader(real, expectedValue);
108116
const expectedHeaderNames = [real];
@@ -122,7 +130,12 @@ server.listen(0, common.mustCall(function() {
122130
response.on('finish', common.mustCall(function() {
123131
assert.strictEqual(response.code, h2.constants.NGHTTP2_NO_ERROR);
124132
assert.strictEqual(response.headersSent, true);
125-
server.close();
133+
process.nextTick(() => {
134+
// can access headersSent after stream is undefined
135+
assert.strictEqual(response.stream, undefined);
136+
assert.strictEqual(response.headersSent, true);
137+
server.close();
138+
});
126139
}));
127140
response.end();
128141
}));

test/parallel/test-http2-compat-serverresponse-settimeout.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
const common = require('../common');
55
if (!common.hasCrypto)
66
common.skip('missing crypto');
7+
const assert = require('assert');
78
const http2 = require('http2');
89

10+
const msecs = common.platformTimeout(1);
911
const server = http2.createServer();
1012

1113
server.on('request', (req, res) => {
12-
res.setTimeout(common.platformTimeout(1), common.mustCall(() => {
14+
res.setTimeout(msecs, common.mustCall(() => {
1315
res.end();
16+
res.setTimeout(msecs, common.mustNotCall());
17+
}));
18+
res.on('finish', common.mustCall(() => {
19+
res.setTimeout(msecs, common.mustNotCall());
20+
process.nextTick(() => {
21+
assert.doesNotThrow(
22+
() => res.setTimeout(msecs, common.mustNotCall())
23+
);
24+
server.close();
25+
});
1426
}));
1527
});
1628

@@ -24,7 +36,6 @@ server.listen(0, common.mustCall(() => {
2436
':authority': `localhost:${port}`
2537
});
2638
req.on('end', common.mustCall(() => {
27-
server.close();
2839
client.destroy();
2940
}));
3041
req.resume();

0 commit comments

Comments
 (0)