-
Notifications
You must be signed in to change notification settings - Fork 30.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http2: support generic Duplex
streams
#16269
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ const tls = require('tls'); | |
const util = require('util'); | ||
const fs = require('fs'); | ||
const errors = require('internal/errors'); | ||
const { StreamWrap } = require('_stream_wrap'); | ||
const { Duplex } = require('stream'); | ||
const { URL } = require('url'); | ||
const { onServerStream, | ||
|
@@ -695,10 +696,14 @@ class Http2Session extends EventEmitter { | |
|
||
// type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT | ||
// options { Object } | ||
// socket { net.Socket | tls.TLSSocket } | ||
// socket { net.Socket | tls.TLSSocket | stream.Duplex } | ||
constructor(type, options, socket) { | ||
super(); | ||
|
||
if (!socket._handle || !socket._handle._externalStream) { | ||
socket = new StreamWrap(socket); | ||
} | ||
|
||
// No validation is performed on the input parameters because this | ||
// constructor is not exported directly for users. | ||
|
||
|
@@ -723,7 +728,8 @@ class Http2Session extends EventEmitter { | |
this[kSocket] = socket; | ||
|
||
// Do not use nagle's algorithm | ||
socket.setNoDelay(); | ||
if (typeof socket.setNoDelay === 'function') | ||
socket.setNoDelay(); | ||
|
||
// Disable TLS renegotiation on the socket | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit further down in this code there is a check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m not sure what the alternative would be. We could document There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would certainly be an option, but given that we're wrapping the custom socket, the likelihood of it even being an issue is pretty low. Let's go with this for now :-) |
||
if (typeof socket.disableRenegotiation === 'function') | ||
|
@@ -2429,15 +2435,19 @@ function connect(authority, options, listener) { | |
const host = authority.hostname || authority.host || 'localhost'; | ||
|
||
let socket; | ||
switch (protocol) { | ||
case 'http:': | ||
socket = net.connect(port, host); | ||
break; | ||
case 'https:': | ||
socket = tls.connect(port, host, initializeTLSOptions(options, host)); | ||
break; | ||
default: | ||
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol); | ||
if (typeof options.createConnection === 'function') { | ||
socket = options.createConnection(authority, options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should likely type check the return value There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all we could do here is doing some duck-type checking, e.g. verifying that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to? This is very much opt-in... What does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn’t do any explicit typechecking either, yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. I won't block on that. I much prefer the APIs to be stricter here and not gate based on what we do in http1 but I can live with this. |
||
} else { | ||
switch (protocol) { | ||
case 'http:': | ||
socket = net.connect(port, host); | ||
break; | ||
case 'https:': | ||
socket = tls.connect(port, host, initializeTLSOptions(options, host)); | ||
break; | ||
default: | ||
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol); | ||
} | ||
} | ||
|
||
socket.on('error', socketOnError); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* eslint-disable required-modules */ | ||
'use strict'; | ||
const { Duplex } = require('stream'); | ||
const assert = require('assert'); | ||
|
||
const kCallback = Symbol('Callback'); | ||
const kOtherSide = Symbol('Other'); | ||
|
||
class DuplexSocket extends Duplex { | ||
constructor() { | ||
super(); | ||
this[kCallback] = null; | ||
this[kOtherSide] = null; | ||
} | ||
|
||
_read() { | ||
const callback = this[kCallback]; | ||
if (callback) { | ||
this[kCallback] = null; | ||
callback(); | ||
} | ||
} | ||
|
||
_write(chunk, encoding, callback) { | ||
assert.notStrictEqual(this[kOtherSide], null); | ||
assert.strictEqual(this[kOtherSide][kCallback], null); | ||
this[kOtherSide][kCallback] = callback; | ||
this[kOtherSide].push(chunk); | ||
} | ||
|
||
_final(callback) { | ||
this[kOtherSide].on('end', callback); | ||
this[kOtherSide].push(null); | ||
} | ||
} | ||
|
||
function makeDuplexPair() { | ||
const clientSide = new DuplexSocket(); | ||
const serverSide = new DuplexSocket(); | ||
clientSide[kOtherSide] = serverSide; | ||
serverSide[kOtherSide] = clientSide; | ||
return { clientSide, serverSide }; | ||
} | ||
|
||
module.exports = makeDuplexPair; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
if (!common.hasCrypto) | ||
common.skip('missing crypto'); | ||
const assert = require('assert'); | ||
const http2 = require('http2'); | ||
const fs = require('fs'); | ||
const makeDuplexPair = require('../common/duplexpair'); | ||
|
||
{ | ||
const server = http2.createServer(); | ||
server.on('stream', common.mustCall((stream, headers) => { | ||
stream.respondWithFile(__filename); | ||
})); | ||
|
||
const { clientSide, serverSide } = makeDuplexPair(); | ||
server.emit('connection', serverSide); | ||
|
||
const client = http2.connect('http://localhost:80', { | ||
createConnection: common.mustCall(() => clientSide) | ||
}); | ||
|
||
const req = client.request({ ':path': '/' }); | ||
|
||
req.on('response', common.mustCall((headers) => { | ||
assert.strictEqual(headers[':status'], 200); | ||
})); | ||
|
||
req.setEncoding('utf8'); | ||
let data = ''; | ||
req.on('data', (chunk) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: ideally a mustCall? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just accumulating the data, and that’s checked in an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mustCall's on data events can be problematic. If anything, a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, this was strictly about the consistency of these calls. In other tests with this PR, data events have When I started writing tests and contributing to node I had a bit of confusion around this because it wasn't 100% consistent across tests. But as I mentioned, this is just a very minor nit. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fwiw, the other call is somewhat intentionally checking that the data is sent through in a single chunk :) I’ve added 2a666de1940b as a comment for that |
||
data += chunk; | ||
}); | ||
req.on('end', common.mustCall(() => { | ||
assert.strictEqual(data, fs.readFileSync(__filename, 'utf8')); | ||
clientSide.destroy(); | ||
clientSide.end(); | ||
})); | ||
req.end(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is super nit picky but could we compare to
undefined
? Anytime we're dealing with an object, because of the existence ofdocument.all
(at least as far as V8 is concerned), these truthy/falsey checks are unusually expensive.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some things in Node’s source explicitly set
._handle
on sockets tonull
(e.g. after disconnecting)… so I’d like to be consistent with TLS here, just to be on the safe side.I doubt this makes a noticeable difference compared to the overall cost of setting up the HTTP2 session objects anyway, V8 seems to optimize a function containing only this line reasonable well to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason I thought we were unsetting
_handle
but if we donull
it then this is fine.