Skip to content

Commit ab16eec

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 e340a66 commit ab16eec

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,
@@ -683,10 +684,14 @@ class Http2Session extends EventEmitter {
683684

684685
// type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT
685686
// options { Object }
686-
// socket { net.Socket | tls.TLSSocket }
687+
// socket { net.Socket | tls.TLSSocket | stream.Duplex }
687688
constructor(type, options, socket) {
688689
super();
689690

691+
if (!socket._handle || !socket._handle._externalStream) {
692+
socket = new StreamWrap(socket);
693+
}
694+
690695
// No validation is performed on the input parameters because this
691696
// constructor is not exported directly for users.
692697

@@ -711,7 +716,8 @@ class Http2Session extends EventEmitter {
711716
this[kSocket] = socket;
712717

713718
// Do not use nagle's algorithm
714-
socket.setNoDelay();
719+
if (typeof socket.setNoDelay === 'function')
720+
socket.setNoDelay();
715721

716722
// Disable TLS renegotiation on the socket
717723
if (typeof socket.disableRenegotiation === 'function')
@@ -2417,15 +2423,19 @@ function connect(authority, options, listener) {
24172423
const host = authority.hostname || authority.host || 'localhost';
24182424

24192425
let socket;
2420-
switch (protocol) {
2421-
case 'http:':
2422-
socket = net.connect(port, host);
2423-
break;
2424-
case 'https:':
2425-
socket = tls.connect(port, host, initializeTLSOptions(options, host));
2426-
break;
2427-
default:
2428-
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
2426+
if (typeof options.createConnection === 'function') {
2427+
socket = options.createConnection(authority, options);
2428+
} else {
2429+
switch (protocol) {
2430+
case 'http:':
2431+
socket = net.connect(port, host);
2432+
break;
2433+
case 'https:':
2434+
socket = tls.connect(port, host, initializeTLSOptions(options, host));
2435+
break;
2436+
default:
2437+
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
2438+
}
24292439
}
24302440

24312441
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)