Skip to content

Commit 966e621

Browse files
committed
http2: support generic Duplex streams
Support generic `Duplex` streams through using `StreamWrap` on the server and client sides, and adding a `createConnection` method option similar to what the HTTP/1 API provides. Since HTTP2 is, as a protocol, independent of its underlying transport layer, Node.js should not enforce any restrictions on what streams its internals may use. Ref: #16256 PR-URL: #16269 Reviewed-By: Anatoli Papirovski <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent ac1b475 commit 966e621

File tree

4 files changed

+109
-11
lines changed

4 files changed

+109
-11
lines changed

doc/api/http2.md

+3
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,9 @@ added: v8.4.0
15981598
used to determine the padding. See [Using options.selectPadding][].
15991599
* `settings` {[Settings Object][]} The initial settings to send to the
16001600
remote peer upon connection.
1601+
* `createConnection` {Function} An optional callback that receives the `URL`
1602+
instance passed to `connect` and the `options` object, and returns any
1603+
[`Duplex`][] stream that is to be used as the connection for this session.
16011604
* `listener` {Function}
16021605
* Returns {Http2Session}
16031606

lib/internal/http2/core.js

+21-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const tls = require('tls');
1313
const util = require('util');
1414
const fs = require('fs');
1515
const errors = require('internal/errors');
16+
const { StreamWrap } = require('_stream_wrap');
1617
const { Duplex } = require('stream');
1718
const { URL } = require('url');
1819
const { onServerStream,
@@ -695,10 +696,14 @@ class Http2Session extends EventEmitter {
695696

696697
// type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT
697698
// options { Object }
698-
// socket { net.Socket | tls.TLSSocket }
699+
// socket { net.Socket | tls.TLSSocket | stream.Duplex }
699700
constructor(type, options, socket) {
700701
super();
701702

703+
if (!socket._handle || !socket._handle._externalStream) {
704+
socket = new StreamWrap(socket);
705+
}
706+
702707
// No validation is performed on the input parameters because this
703708
// constructor is not exported directly for users.
704709

@@ -723,7 +728,8 @@ class Http2Session extends EventEmitter {
723728
this[kSocket] = socket;
724729

725730
// Do not use nagle's algorithm
726-
socket.setNoDelay();
731+
if (typeof socket.setNoDelay === 'function')
732+
socket.setNoDelay();
727733

728734
// Disable TLS renegotiation on the socket
729735
if (typeof socket.disableRenegotiation === 'function')
@@ -2429,15 +2435,19 @@ function connect(authority, options, listener) {
24292435
const host = authority.hostname || authority.host || 'localhost';
24302436

24312437
let socket;
2432-
switch (protocol) {
2433-
case 'http:':
2434-
socket = net.connect(port, host);
2435-
break;
2436-
case 'https:':
2437-
socket = tls.connect(port, host, initializeTLSOptions(options, host));
2438-
break;
2439-
default:
2440-
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
2438+
if (typeof options.createConnection === 'function') {
2439+
socket = options.createConnection(authority, options);
2440+
} else {
2441+
switch (protocol) {
2442+
case 'http:':
2443+
socket = net.connect(port, host);
2444+
break;
2445+
case 'https:':
2446+
socket = tls.connect(port, host, initializeTLSOptions(options, host));
2447+
break;
2448+
default:
2449+
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
2450+
}
24412451
}
24422452

24432453
socket.on('error', socketOnError);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
const assert = require('assert');
6+
const http2 = require('http2');
7+
const fs = require('fs');
8+
const makeDuplexPair = require('../common/duplexpair');
9+
10+
{
11+
const server = http2.createServer();
12+
server.on('stream', common.mustCall((stream, headers) => {
13+
stream.respondWithFile(__filename);
14+
}));
15+
16+
const { clientSide, serverSide } = makeDuplexPair();
17+
server.emit('connection', serverSide);
18+
19+
const client = http2.connect('http://localhost:80', {
20+
createConnection: common.mustCall(() => clientSide)
21+
});
22+
23+
const req = client.request({ ':path': '/' });
24+
25+
req.on('response', common.mustCall((headers) => {
26+
assert.strictEqual(headers[':status'], 200);
27+
}));
28+
29+
req.setEncoding('utf8');
30+
let data = '';
31+
req.on('data', (chunk) => {
32+
data += chunk;
33+
});
34+
req.on('end', common.mustCall(() => {
35+
assert.strictEqual(data, fs.readFileSync(__filename, 'utf8'));
36+
clientSide.destroy();
37+
clientSide.end();
38+
}));
39+
req.end();
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
const assert = require('assert');
6+
const http2 = require('http2');
7+
const makeDuplexPair = require('../common/duplexpair');
8+
9+
{
10+
const testData = '<h1>Hello World</h1>';
11+
const server = http2.createServer();
12+
server.on('stream', common.mustCall((stream, headers) => {
13+
stream.respond({
14+
'content-type': 'text/html',
15+
':status': 200
16+
});
17+
stream.end(testData);
18+
}));
19+
20+
const { clientSide, serverSide } = makeDuplexPair();
21+
server.emit('connection', serverSide);
22+
23+
const client = http2.connect('http://localhost:80', {
24+
createConnection: common.mustCall(() => clientSide)
25+
});
26+
27+
const req = client.request({ ':path': '/' });
28+
29+
req.on('response', common.mustCall((headers) => {
30+
assert.strictEqual(headers[':status'], 200);
31+
}));
32+
33+
req.setEncoding('utf8');
34+
// Note: This is checking that this small amount of data is passed through in
35+
// a single chunk, which is unusual for our test suite but seems like a
36+
// reasonable assumption here.
37+
req.on('data', common.mustCall((data) => {
38+
assert.strictEqual(data, testData);
39+
}));
40+
req.on('end', common.mustCall(() => {
41+
clientSide.destroy();
42+
clientSide.end();
43+
}));
44+
req.end();
45+
}

0 commit comments

Comments
 (0)