From c76cf39e4a6b85aab601120f6f2804b8194f5cb8 Mon Sep 17 00:00:00 2001 From: Anatoli Papirovski Date: Wed, 9 May 2018 12:12:43 +0200 Subject: [PATCH 1/2] http2: fix end without read Adjust http2 behaviour to allow ending a stream even after some data comes in (when the user has no intention of reading that data). Also correctly end a stream when trailers are present. --- lib/internal/http2/compat.js | 10 +++-- lib/internal/http2/core.js | 8 ++-- .../test-http2-client-upload-reject.js | 15 ++++--- .../test-http2-compat-client-upload-reject.js | 44 +++++++++++++++++++ 4 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 test/parallel/test-http2-compat-client-upload-reject.js diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index d2aaa8838e2cbc..92b7fd4a3296b2 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -240,8 +240,6 @@ class Http2ServerRequest extends Readable { stream[kRequest] = this; // Pause the stream.. - stream.pause(); - stream.on('data', onStreamData); stream.on('trailers', onStreamTrailers); stream.on('end', onStreamEnd); stream.on('error', onStreamError); @@ -308,8 +306,12 @@ class Http2ServerRequest extends Readable { _read(nread) { const state = this[kState]; if (!state.closed) { - state.didRead = true; - process.nextTick(resumeStream, this[kStream]); + if (!state.didRead) { + state.didRead = true; + this[kStream].on('data', onStreamData); + } else { + process.nextTick(resumeStream, this[kStream]); + } } else { this.emit('error', new ERR_HTTP2_INVALID_STREAM()); } diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index ede4f5d34371e8..bf74af1289d7e9 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -351,10 +351,8 @@ function onStreamClose(code) { // it completely. stream.push(null); - // Same as net. - if (stream.readableLength === 0) { - stream.read(0); - } + if (!stream[kState].didRead && !stream._readableState.resumeScheduled) + stream.resume(); } } @@ -1796,6 +1794,8 @@ class Http2Stream extends Duplex { const ret = this[kHandle].trailers(headersList); if (ret < 0) this.destroy(new NghttpError(ret)); + else + this[kMaybeDestroy](); } get closed() { diff --git a/test/parallel/test-http2-client-upload-reject.js b/test/parallel/test-http2-client-upload-reject.js index ece7cbdf233f1f..678114130e3dba 100644 --- a/test/parallel/test-http2-client-upload-reject.js +++ b/test/parallel/test-http2-client-upload-reject.js @@ -20,12 +20,15 @@ fs.readFile(loc, common.mustCall((err, data) => { const server = http2.createServer(); server.on('stream', common.mustCall((stream) => { - stream.on('close', common.mustCall(() => { - assert.strictEqual(stream.rstCode, 0); - })); - - stream.respond({ ':status': 400 }); - stream.end(); + // Wait for some data to come through. + setImmediate(() => { + stream.on('close', common.mustCall(() => { + assert.strictEqual(stream.rstCode, 0); + })); + + stream.respond({ ':status': 400 }); + stream.end(); + }); })); server.listen(0, common.mustCall(() => { diff --git a/test/parallel/test-http2-compat-client-upload-reject.js b/test/parallel/test-http2-compat-client-upload-reject.js new file mode 100644 index 00000000000000..e6a187cb12b264 --- /dev/null +++ b/test/parallel/test-http2-compat-client-upload-reject.js @@ -0,0 +1,44 @@ +'use strict'; + +// Verifies that uploading data from a client works + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); + +const loc = fixtures.path('person-large.jpg'); + +assert(fs.existsSync(loc)); + +fs.readFile(loc, common.mustCall((err, data) => { + assert.ifError(err); + + const server = http2.createServer(common.mustCall((req, res) => { + setImmediate(() => { + res.writeHead(400); + res.end(); + }); + })); + + server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request({ ':method': 'POST' }); + req.on('response', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 400); + })); + + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + + const str = fs.createReadStream(loc); + str.pipe(req); + })); +})); From 504055fe7c49ee506d0be8afd1835a8ac4880c34 Mon Sep 17 00:00:00 2001 From: Anatoli Papirovski Date: Thu, 17 May 2018 13:14:44 +0400 Subject: [PATCH 2/2] fixup: add a comment --- lib/internal/http2/core.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index bf74af1289d7e9..499381d118ee3c 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -350,7 +350,9 @@ function onStreamClose(code) { // Push a null so the stream can end whenever the client consumes // it completely. stream.push(null); - + // If the client hasn't tried to consume the stream and there is no + // resume scheduled (which would indicate they would consume in the future), + // then just dump the incoming data so that the stream can be destroyed. if (!stream[kState].didRead && !stream._readableState.resumeScheduled) stream.resume(); }