-
Notifications
You must be signed in to change notification settings - Fork 31.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: fix compat stream read handling, add tests #15503
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 |
---|---|---|
|
@@ -97,46 +97,50 @@ function onStreamError(error) { | |
} | ||
|
||
function onRequestPause() { | ||
this[kStream].pause(); | ||
const stream = this[kStream]; | ||
if (stream) | ||
stream.pause(); | ||
} | ||
|
||
function onRequestResume() { | ||
this[kStream].resume(); | ||
} | ||
|
||
function onRequestDrain() { | ||
if (this.isPaused()) | ||
this.resume(); | ||
const stream = this[kStream]; | ||
if (stream) | ||
stream.resume(); | ||
} | ||
|
||
function onStreamResponseDrain() { | ||
function onStreamDrain() { | ||
this[kResponse].emit('drain'); | ||
} | ||
|
||
// TODO Http2Stream does not emit 'close' | ||
function onStreamClosedRequest() { | ||
this[kRequest].push(null); | ||
} | ||
|
||
// TODO Http2Stream does not emit 'close' | ||
function onStreamClosedResponse() { | ||
const res = this[kResponse]; | ||
res.writable = false; | ||
res.emit('finish'); | ||
this[kResponse].emit('finish'); | ||
} | ||
|
||
function onStreamAbortedRequest(hadError, code) { | ||
if ((this.writable) || | ||
(this._readableState && !this._readableState.ended)) { | ||
this.emit('aborted', hadError, code); | ||
this.emit('close'); | ||
const request = this[kRequest]; | ||
if (request[kState].closed === false) { | ||
request.emit('aborted', hadError, code); | ||
request.emit('close'); | ||
} | ||
} | ||
|
||
function onStreamAbortedResponse() { | ||
if (this.writable) { | ||
this.emit('close'); | ||
const response = this[kResponse]; | ||
if (response[kState].closed === false) { | ||
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.
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. 👍 |
||
response.emit('close'); | ||
} | ||
} | ||
|
||
function resumeStream(stream) { | ||
stream.resume(); | ||
} | ||
|
||
class Http2ServerRequest extends Readable { | ||
constructor(stream, headers, options, rawHeaders) { | ||
super(options); | ||
|
@@ -158,13 +162,12 @@ class Http2ServerRequest extends Readable { | |
stream.on('end', onStreamEnd); | ||
stream.on('error', onStreamError); | ||
stream.on('close', onStreamClosedRequest); | ||
stream.on('aborted', onStreamAbortedRequest.bind(this)); | ||
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. Any |
||
stream.on('aborted', onStreamAbortedRequest); | ||
const onfinish = this[kFinish].bind(this); | ||
stream.on('streamClosed', onfinish); | ||
stream.on('finish', onfinish); | ||
this.on('pause', onRequestPause); | ||
this.on('resume', onRequestResume); | ||
this.on('drain', onRequestDrain); | ||
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 isn't an event on Readable. |
||
} | ||
|
||
get closed() { | ||
|
@@ -221,7 +224,7 @@ class Http2ServerRequest extends Readable { | |
_read(nread) { | ||
const stream = this[kStream]; | ||
if (stream !== undefined) { | ||
stream.resume(); | ||
process.nextTick(resumeStream, stream); | ||
} else { | ||
this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED')); | ||
} | ||
|
@@ -279,9 +282,9 @@ class Http2ServerResponse extends Stream { | |
this[kStream] = stream; | ||
stream[kResponse] = this; | ||
this.writable = true; | ||
stream.on('drain', onStreamResponseDrain); | ||
stream.on('drain', onStreamDrain); | ||
stream.on('close', onStreamClosedResponse); | ||
stream.on('aborted', onStreamAbortedResponse.bind(this)); | ||
stream.on('aborted', onStreamAbortedResponse); | ||
const onfinish = this[kFinish].bind(this); | ||
stream.on('streamClosed', onfinish); | ||
stream.on('finish', onfinish); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Flags: --expose-http2 | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
if (!common.hasCrypto) | ||
common.skip('missing crypto'); | ||
const assert = require('assert'); | ||
const h2 = require('http2'); | ||
|
||
// Check that pause & resume work as expected with Http2ServerRequest | ||
|
||
const testStr = 'Request Body from Client'; | ||
|
||
const server = h2.createServer(); | ||
|
||
server.on('request', common.mustCall((req, res) => { | ||
let data = ''; | ||
req.pause(); | ||
req.setEncoding('utf8'); | ||
req.on('data', common.mustCall((chunk) => (data += chunk))); | ||
setTimeout(common.mustCall(() => { | ||
assert.strictEqual(data, ''); | ||
req.resume(); | ||
}), common.platformTimeout(100)); | ||
req.on('end', common.mustCall(() => { | ||
assert.strictEqual(data, testStr); | ||
res.end(); | ||
})); | ||
|
||
// shouldn't throw if underlying Http2Stream no longer exists | ||
res.on('finish', common.mustCall(() => process.nextTick(() => { | ||
assert.doesNotThrow(() => req.pause()); | ||
assert.doesNotThrow(() => req.resume()); | ||
}))); | ||
})); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
const port = server.address().port; | ||
|
||
const client = h2.connect(`http://localhost:${port}`); | ||
const request = client.request({ | ||
':path': '/foobar', | ||
':method': 'POST', | ||
':scheme': 'http', | ||
':authority': `localhost:${port}` | ||
}); | ||
request.resume(); | ||
request.end(testStr); | ||
request.on('end', common.mustCall(function() { | ||
client.destroy(); | ||
server.close(); | ||
})); | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Flags: --expose-http2 | ||
'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 path = require('path'); | ||
|
||
// piping should work as expected with createWriteStream | ||
|
||
const loc = path.join(common.fixturesDir, 'person.jpg'); | ||
const fn = path.join(common.tmpDir, 'http2pipe.jpg'); | ||
common.refreshTmpDir(); | ||
|
||
const server = http2.createServer(); | ||
|
||
server.on('request', common.mustCall((req, res) => { | ||
const dest = req.pipe(fs.createWriteStream(fn)); | ||
dest.on('finish', common.mustCall(() => { | ||
assert.deepStrictEqual(fs.readFileSync(loc), fs.readFileSync(fn)); | ||
fs.unlinkSync(fn); | ||
res.end(); | ||
})); | ||
})); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
const port = server.address().port; | ||
const client = http2.connect(`http://localhost:${port}`); | ||
|
||
let remaining = 2; | ||
function maybeClose() { | ||
if (--remaining === 0) { | ||
server.close(); | ||
client.destroy(); | ||
} | ||
} | ||
|
||
const req = client.request({ ':method': 'POST' }); | ||
req.on('response', common.mustCall()); | ||
req.resume(); | ||
req.on('end', common.mustCall(maybeClose)); | ||
const str = fs.createReadStream(loc); | ||
str.on('end', common.mustCall(maybeClose)); | ||
str.pipe(req); | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Flags: --expose-http2 | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
if (!common.hasCrypto) | ||
common.skip('missing crypto'); | ||
const assert = require('assert'); | ||
const h2 = require('http2'); | ||
|
||
// Check that drain event is passed from Http2Stream | ||
|
||
const testString = 'tests'; | ||
|
||
const server = h2.createServer(); | ||
|
||
server.on('request', common.mustCall((req, res) => { | ||
res.stream._writableState.highWaterMark = testString.length; | ||
assert.strictEqual(res.write(testString), false); | ||
res.on('drain', common.mustCall(() => res.end(testString))); | ||
})); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
const port = server.address().port; | ||
|
||
const client = h2.connect(`http://localhost:${port}`); | ||
const request = client.request({ | ||
':path': '/foobar', | ||
':method': 'POST', | ||
':scheme': 'http', | ||
':authority': `localhost:${port}` | ||
}); | ||
request.resume(); | ||
request.end(); | ||
|
||
let data = ''; | ||
request.setEncoding('utf8'); | ||
request.on('data', (chunk) => (data += chunk)); | ||
|
||
request.on('end', common.mustCall(function() { | ||
assert.strictEqual(data, testString.repeat(2)); | ||
client.destroy(); | ||
server.close(); | ||
})); | ||
})); |
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 can remove these comments but I wanted it clear that these two don't run as things are. It's a bit confusing otherwise.
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.
@jasnell can we remove those then? Or do you prefer if we add 'close' to Http2Stream?
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'm good with removing the comments.
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.
oh lol... heh Matteo pointed out that I didn't understand the comments :-) This is true, I missed that.
I'm thinking we need to refactor this so that
streamClosed
andclose
are one and the same and thatclose
is emitted appropriately. The one bit we'd have to account for is thecode
that is reported instreamClosed
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 could be wrong but I think @mcollina is asking about removing the functions which the comments refer to. If a 'close' event will be added to Http2Stream at some point then they should be kept but if that's not going to happen then we can safely remove.too slow lol