From ca21bd1bf2c9c8ad83ee30d4d86c4eb994144c4d Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 14 Jun 2020 11:10:32 -0400 Subject: [PATCH 01/14] http2: use and support non-empty DATA frame with END_STREAM flag Adds support for reading from a stream where the final frame is a non-empty DATA frame with the END_STREAM flag set, instead of hanging waiting for another frame. When writing to a stream, uses a END_STREAM flag on final DATA frame instead of adding an empty DATA frame. BREAKING: http2 client now expects servers to properly support END_STREAM flag Fixes: https://github.com/nodejs/node/issues/31309 Fixes: https://github.com/nodejs/node/issues/33891 Refs: https://nghttp2.org/documentation/types.html#c.nghttp2_on_data_chunk_recv_callback --- src/node_http2.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/node_http2.cc b/src/node_http2.cc index 4d1309e08a1a78..110aee87388367 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1089,6 +1089,12 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle, session->SendPendingData(); } } while (len != 0); + + // If end-stream flag is set, there is nothing more to read + if (flags & NGHTTP2_FLAG_END_STREAM) { + stream->EmitRead(UV_EOF); + return 0; + } // If we are currently waiting for a write operation to finish, we should // tell nghttp2 that we want to wait before we process more input data. From 1e35afd326b9805fee575541da7d29ff26478050 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 14 Jun 2020 12:42:58 -0400 Subject: [PATCH 02/14] remove whitespace --- src/node_http2.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index 110aee87388367..d28f30406f8742 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1089,7 +1089,7 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle, session->SendPendingData(); } } while (len != 0); - + // If end-stream flag is set, there is nothing more to read if (flags & NGHTTP2_FLAG_END_STREAM) { stream->EmitRead(UV_EOF); From 30b1c06ae04c20b7da32ef6498c9f0de16d66664 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 14 Jun 2020 19:50:38 -0400 Subject: [PATCH 03/14] Update node_http2.cc --- src/node_http2.cc | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index d28f30406f8742..822259b772780a 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -755,7 +755,7 @@ ssize_t Http2Session::ConsumeHTTP2Data() { CHECK_GT(ret, 0); CHECK_LE(static_cast(ret), read_len); - if (static_cast(ret) < read_len) { + if (static_cast(ret) <= read_len) { // Mark the remainder of the data as available for later consumption. stream_buf_offset_ += ret; return ret; @@ -1090,12 +1090,6 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle, } } while (len != 0); - // If end-stream flag is set, there is nothing more to read - if (flags & NGHTTP2_FLAG_END_STREAM) { - stream->EmitRead(UV_EOF); - return 0; - } - // If we are currently waiting for a write operation to finish, we should // tell nghttp2 that we want to wait before we process more input data. if (session->is_write_in_progress()) { From 6f7c361bbf0e08045a8c916c8539d47d524fbeed Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 14 Jun 2020 21:23:13 -0400 Subject: [PATCH 04/14] Update node_http2.cc --- src/node_http2.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index 822259b772780a..18e8774c154428 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -734,7 +734,7 @@ ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, // quite expensive. This is a potential performance optimization target later. ssize_t Http2Session::ConsumeHTTP2Data() { CHECK_NOT_NULL(stream_buf_.base); - CHECK_LT(stream_buf_offset_, stream_buf_.len); + CHECK_LE(stream_buf_offset_, stream_buf_.len); size_t read_len = stream_buf_.len - stream_buf_offset_; // multiple side effects. From cbddeb6a1d1270c900e6251879f2a453b23f3ce9 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 15 Jun 2020 06:54:38 -0400 Subject: [PATCH 05/14] Update node_http2.cc --- src/node_http2.cc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index 18e8774c154428..a1a9dfef3b535c 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -755,11 +755,10 @@ ssize_t Http2Session::ConsumeHTTP2Data() { CHECK_GT(ret, 0); CHECK_LE(static_cast(ret), read_len); - if (static_cast(ret) <= read_len) { - // Mark the remainder of the data as available for later consumption. - stream_buf_offset_ += ret; - return ret; - } + // Mark the remainder of the data as available for later consumption. + // Even if all bytes were received, a paused stream may delay nghttp2_on_frame_recv_callback + stream_buf_offset_ += ret; + return ret; } // We are done processing the current input chunk. From acb599956ca3b60154352e8fb4e09be761047dec Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 15 Jun 2020 14:24:17 -0400 Subject: [PATCH 06/14] Update node_http2.cc --- src/node_http2.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index a1a9dfef3b535c..77e9fe22c91c70 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -756,7 +756,8 @@ ssize_t Http2Session::ConsumeHTTP2Data() { CHECK_LE(static_cast(ret), read_len); // Mark the remainder of the data as available for later consumption. - // Even if all bytes were received, a paused stream may delay nghttp2_on_frame_recv_callback + // Even if all bytes were received, a paused stream may delay + // nghttp2_on_frame_recv_callback which may have an END_STREAM flag. stream_buf_offset_ += ret; return ret; } From 85cf7224f934037fda562069074b724ba7d25fa3 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Mon, 15 Jun 2020 18:44:41 -0400 Subject: [PATCH 07/14] Update node_http2.cc --- src/node_http2.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node_http2.cc b/src/node_http2.cc index 77e9fe22c91c70..4d48a4d9b4ac27 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -756,7 +756,7 @@ ssize_t Http2Session::ConsumeHTTP2Data() { CHECK_LE(static_cast(ret), read_len); // Mark the remainder of the data as available for later consumption. - // Even if all bytes were received, a paused stream may delay + // Even if all bytes were received, a paused stream may delay the // nghttp2_on_frame_recv_callback which may have an END_STREAM flag. stream_buf_offset_ += ret; return ret; From 7bae5a5535a8ef1bbf9ae5639b4f21566c65f622 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Tue, 16 Jun 2020 10:06:12 -0400 Subject: [PATCH 08/14] http2: add "receive paused" debug native line --- src/node_http2.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node_http2.cc b/src/node_http2.cc index 4d48a4d9b4ac27..8d2575557917c2 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -1095,6 +1095,7 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle, if (session->is_write_in_progress()) { CHECK(session->is_reading_stopped()); session->set_receive_paused(); + Debug(session, "receive paused"); return NGHTTP2_ERR_PAUSE; } From 789cc1dbe3b861dd053ab84455180396e4fbc5c9 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Wed, 17 Jun 2020 17:57:24 -0400 Subject: [PATCH 09/14] http2: remove empty DATA frame --- lib/internal/http2/core.js | 42 +++++++++++++++++++++++--- test/parallel/test-http2-perf_hooks.js | 2 +- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 1503130f9b4752..31101dc9d8fdc1 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1978,19 +1978,51 @@ class Http2Stream extends Duplex { let req; - // writeGeneric does not destroy on error and we cannot enable autoDestroy, - // so make sure to destroy on error. - const callback = (err) => { + let waitingForWriteCallback = true; + let waitingForEndCheck = true; + let writeCallbackErr; + let endCheckCallbackErr; + const done = () => { + if (waitingForEndCheck || waitingForWriteCallback) return; + const err = writeCallbackErr || endCheckCallbackErr; + + // writeGeneric does not destroy on error and + // we cannot enable autoDestroy, + // so make sure to destroy on error. if (err) { this.destroy(err); } cb(err); }; + const writeCallback = (err) => { + waitingForWriteCallback = false; + writeCallbackErr = err; + done(); + }; + const endCheckCallback = (err) => { + waitingForEndCheck = false; + endCheckCallbackErr = err; + done(); + }; + process.nextTick(() => { + if (!this._writableState.ending) return endCheckCallback(); + if (this._writableState.buffered.length) return endCheckCallback(); + const handle = this[kHandle]; + if (!handle) return endCheckCallback(); + debugStreamObj(this, 'kWriteGeneric shutting down'); + const req = new ShutdownWrap(); + req.oncomplete = afterShutdown; + req.callback = endCheckCallback; + req.handle = handle; + const err = handle.shutdown(req); + if (err === 1) // synchronous finish + return afterShutdown.call(req, 0); + }); if (writev) - req = writevGeneric(this, data, callback); + req = writevGeneric(this, data, writeCallback); else - req = writeGeneric(this, data, encoding, callback); + req = writeGeneric(this, data, encoding, writeCallback); trackWriteState(this, req.bytes); } diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js index 0fcbc323e01301..1023d70ff73f2c 100644 --- a/test/parallel/test-http2-perf_hooks.js +++ b/test/parallel/test-http2-perf_hooks.js @@ -30,7 +30,7 @@ const obs = new PerformanceObserver(common.mustCall((items) => { break; case 'client': assert.strictEqual(entry.streamCount, 1); - assert.strictEqual(entry.framesReceived, 8); + assert.strictEqual(entry.framesReceived, 7); break; default: assert.fail('invalid Http2Session type'); From 0c110d044a89ce6355003fcc6385694b110ba89a Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Wed, 17 Jun 2020 18:48:16 -0400 Subject: [PATCH 10/14] http2: update tests --- lib/internal/http2/core.js | 10 +++++++--- test/parallel/test-http2-padding-aligned.js | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 31101dc9d8fdc1..30d0c4b0e843c4 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1985,7 +1985,6 @@ class Http2Stream extends Duplex { const done = () => { if (waitingForEndCheck || waitingForWriteCallback) return; const err = writeCallbackErr || endCheckCallbackErr; - // writeGeneric does not destroy on error and // we cannot enable autoDestroy, // so make sure to destroy on error. @@ -2004,9 +2003,14 @@ class Http2Stream extends Duplex { endCheckCallbackErr = err; done(); }; + // Shutdown write stream right after last chunk is sent + // so final DATA frame can include END_STREAM flag process.nextTick(() => { - if (!this._writableState.ending) return endCheckCallback(); - if (this._writableState.buffered.length) return endCheckCallback(); + if (writeCallbackErr || + !this._writableState.ending || + this._writableState.buffered.length || + (this[kState] & STREAM_FLAGS_HAS_TRAILERS)) + return endCheckCallback(); const handle = this[kHandle]; if (!handle) return endCheckCallback(); debugStreamObj(this, 'kWriteGeneric shutting down'); diff --git a/test/parallel/test-http2-padding-aligned.js b/test/parallel/test-http2-padding-aligned.js index 432e3e8629f379..f687c02a98dc6e 100644 --- a/test/parallel/test-http2-padding-aligned.js +++ b/test/parallel/test-http2-padding-aligned.js @@ -26,7 +26,7 @@ const makeDuplexPair = require('../common/duplexpair'); // The lengths of the expected writes... note that this is highly // sensitive to how the internals are implemented. const serverLengths = [24, 9, 9, 32]; - const clientLengths = [9, 9, 48, 9, 1, 21, 1, 16]; + const clientLengths = [9, 9, 48, 9, 1, 21, 1]; // Adjust for the 24-byte preamble and two 9-byte settings frames, and // the result must be equally divisible by 8 From 563a31043b2869a3283e1220c108b90f10695564 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Wed, 17 Jun 2020 20:08:50 -0400 Subject: [PATCH 11/14] fix trailers flag detection --- lib/internal/http2/core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 30d0c4b0e843c4..72961d1e1abc21 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2009,7 +2009,7 @@ class Http2Stream extends Duplex { if (writeCallbackErr || !this._writableState.ending || this._writableState.buffered.length || - (this[kState] & STREAM_FLAGS_HAS_TRAILERS)) + (this[kState].flags & STREAM_FLAGS_HAS_TRAILERS)) return endCheckCallback(); const handle = this[kHandle]; if (!handle) return endCheckCallback(); From 327e9e8baba3741635afb7fa83c095d0da71401e Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Thu, 18 Jun 2020 10:04:59 -0400 Subject: [PATCH 12/14] fix tests --- lib/internal/http2/core.js | 25 ++++++--- .../test-http2-misbehaving-multiplex.js | 56 +++++++++++++------ 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 72961d1e1abc21..b03cf6477f8dc9 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -2158,11 +2158,20 @@ class Http2Stream extends Duplex { debugStream(this[kID] || 'pending', session[kType], 'destroying stream'); const state = this[kState]; - const sessionCode = session[kState].goawayCode || - session[kState].destroyCode; - const code = err != null ? - sessionCode || NGHTTP2_INTERNAL_ERROR : - state.rstCode || sessionCode; + const sessionState = session[kState]; + const sessionCode = sessionState.goawayCode || sessionState.destroyCode; + + // If a stream has already closed successfully, there is no error + // to report from this stream, even if the session has errored. + // This can happen if the stream was already in process of destroying + // after a successful close, but the session had a error between + // this stream's close and destroy operations. + // Previously, this always overrode a successful close operation code + // NGHTTP2_NO_ERROR (0) with sessionCode because the use of the || operator. + const code = (err != null ? + (sessionCode || NGHTTP2_INTERNAL_ERROR) : + (this.closed ? this.rstCode : sessionCode) + ); const hasHandle = handle !== undefined; if (!this.closed) @@ -2171,13 +2180,13 @@ class Http2Stream extends Duplex { if (hasHandle) { handle.destroy(); - session[kState].streams.delete(id); + sessionState.streams.delete(id); } else { - session[kState].pendingStreams.delete(this); + sessionState.pendingStreams.delete(this); } // Adjust the write queue size for accounting - session[kState].writeQueueSize -= state.writeQueueSize; + sessionState.writeQueueSize -= state.writeQueueSize; state.writeQueueSize = 0; // RST code 8 not emitted as an error as its used by clients to signify diff --git a/test/parallel/test-http2-misbehaving-multiplex.js b/test/parallel/test-http2-misbehaving-multiplex.js index fbd8add8906b7e..0e057e1ed28e7a 100644 --- a/test/parallel/test-http2-misbehaving-multiplex.js +++ b/test/parallel/test-http2-misbehaving-multiplex.js @@ -2,6 +2,7 @@ // Flags: --expose-internals const common = require('../common'); +const assert = require('assert'); if (!common.hasCrypto) common.skip('missing crypto'); @@ -13,16 +14,36 @@ const h2test = require('../common/http2'); let client; const server = h2.createServer(); +let gotFirstStreamId1; server.on('stream', common.mustCall((stream) => { stream.respond(); stream.end('ok'); - // The error will be emitted asynchronously - stream.on('error', common.expectsError({ - constructor: NghttpError, - code: 'ERR_HTTP2_ERROR', - message: 'Stream was already closed or invalid' - })); + // Http2Server should be fast enough to respond to and close + // the first streams with ID 1 and ID 3 without errors. + + // Test for errors in 'close' event to ensure no errors on some streams. + stream.on('error', () => {}); + stream.on('close', (err) => { + if (stream.id === 1) { + if (gotFirstStreamId1) { + // We expect our outgoing frames to fail on Stream ID 1 the second time + // because a stream with ID 1 was already closed before. + common.expectsError({ + constructor: NghttpError, + code: 'ERR_HTTP2_ERROR', + message: 'Stream was already closed or invalid' + }); + return; + } + gotFirstStreamId1 = true; + } + assert.strictEqual(err, undefined); + }); + + // Stream ID 5 should never reach the server + assert.notStrictEqual(stream.id, 5); + }, 2)); server.on('session', common.mustCall((session) => { @@ -35,26 +56,27 @@ server.on('session', common.mustCall((session) => { const settings = new h2test.SettingsFrame(); const settingsAck = new h2test.SettingsFrame(true); -const head1 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true); -const head2 = new h2test.HeadersFrame(3, h2test.kFakeRequestHeaders, 0, true); -const head3 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true); -const head4 = new h2test.HeadersFrame(5, h2test.kFakeRequestHeaders, 0, true); +// HeadersFrame(id, payload, padding, END_STREAM) +const id1 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true); +const id3 = new h2test.HeadersFrame(3, h2test.kFakeRequestHeaders, 0, true); +const id5 = new h2test.HeadersFrame(5, h2test.kFakeRequestHeaders, 0, true); server.listen(0, () => { client = net.connect(server.address().port, () => { client.write(h2test.kClientMagic, () => { client.write(settings.data, () => { client.write(settingsAck.data); - // This will make it ok. - client.write(head1.data, () => { - // This will make it ok. - client.write(head2.data, () => { + // Stream ID 1 frame will make it OK. + client.write(id1.data, () => { + // Stream ID 3 frame will make it OK. + client.write(id3.data, () => { + // A second Stream ID 1 frame should fail. // This will cause an error to occur because the client is // attempting to reuse an already closed stream. This must // cause the server session to be torn down. - client.write(head3.data, () => { - // This won't ever make it to the server - client.write(head4.data); + client.write(id1.data, () => { + // This Stream ID 5 frame will never make it to the server + client.write(id5.data); }); }); }); From 58a3885960086c8b7f134684aa8ef2d21a3e43f5 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Thu, 18 Jun 2020 11:18:24 -0400 Subject: [PATCH 13/14] add test --- .../test-http2-pack-end-stream-flag.js | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/parallel/test-http2-pack-end-stream-flag.js diff --git a/test/parallel/test-http2-pack-end-stream-flag.js b/test/parallel/test-http2-pack-end-stream-flag.js new file mode 100644 index 00000000000000..f6bb4452d95a77 --- /dev/null +++ b/test/parallel/test-http2-pack-end-stream-flag.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); + +const { PerformanceObserver } = require('perf_hooks'); + +const server = http2.createServer(); + +server.on('stream', (stream, headers) => { + stream.respond({ + 'content-type': 'text/html', + ':status': 200 + }); + switch (headers[':path']) { + case '/singleEnd': + stream.end('OK'); + break; + case '/sequentialEnd': + stream.write('OK'); + stream.end(); + break; + case '/delayedEnd': + stream.write('OK', () => stream.end()); + break; + } +}); + +function testRequest(path, targetFrameCount, callback) { + const obs = new PerformanceObserver((list, observer) => { + const entry = list.getEntries()[0]; + if (entry.name !== 'Http2Session') return; + if (entry.type !== 'client') return; + assert.strictEqual(entry.framesReceived, targetFrameCount); + observer.disconnect(); + callback(); + }); + obs.observe({ entryTypes: ['http2'] }); + const client = http2.connect(`http://localhost:${server.address().port}`, () => { + const req = client.request({ ':path': path }); + req.resume(); + req.end(); + req.on('end', () => client.close()); + }); +} + +// SETTINGS => SETTINGS => HEADERS => DATA +const MIN_FRAME_COUNT = 4; + +server.listen(0, () => { + testRequest('/singleEnd', MIN_FRAME_COUNT, () => { + testRequest('/sequentialEnd', MIN_FRAME_COUNT, () => { + testRequest('/delayedEnd', MIN_FRAME_COUNT + 1, () => { + server.close(); + }); + }); + }); +}); From 1778fc69f3e51769bd2915096cfd823cd5610ef8 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Jr Date: Fri, 19 Jun 2020 17:30:50 -0400 Subject: [PATCH 14/14] add state.shutdownWritableCalled --- lib/internal/http2/core.js | 47 ++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index b03cf6477f8dc9..6270b60e81ddbb 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1140,6 +1140,7 @@ class Http2Session extends EventEmitter { streams: new Map(), pendingStreams: new Set(), pendingAck: 0, + shutdownWritableCalled: false, writeQueueSize: 0, originSet: undefined }; @@ -1718,6 +1719,25 @@ function afterShutdown(status) { this.callback(); } +function shutdownWritable(callback) { + const handle = this[kHandle]; + if (!handle) return callback(); + const state = this[kState]; + if (state.shutdownWritableCalled) { + debugStreamObj(this, 'shutdownWritable() already called'); + return callback(); + } + state.shutdownWritableCalled = true; + + const req = new ShutdownWrap(); + req.oncomplete = afterShutdown; + req.callback = callback; + req.handle = handle; + const err = handle.shutdown(req); + if (err === 1) // synchronous finish + return afterShutdown.call(req, 0); +} + function finishSendTrailers(stream, headersList) { // The stream might be destroyed and in that case // there is nothing to do. @@ -2011,16 +2031,8 @@ class Http2Stream extends Duplex { this._writableState.buffered.length || (this[kState].flags & STREAM_FLAGS_HAS_TRAILERS)) return endCheckCallback(); - const handle = this[kHandle]; - if (!handle) return endCheckCallback(); - debugStreamObj(this, 'kWriteGeneric shutting down'); - const req = new ShutdownWrap(); - req.oncomplete = afterShutdown; - req.callback = endCheckCallback; - req.handle = handle; - const err = handle.shutdown(req); - if (err === 1) // synchronous finish - return afterShutdown.call(req, 0); + debugStreamObj(this, 'shutting down writable on last write'); + shutdownWritable.call(this, endCheckCallback); }); if (writev) @@ -2040,21 +2052,12 @@ class Http2Stream extends Duplex { } _final(cb) { - const handle = this[kHandle]; if (this.pending) { this.once('ready', () => this._final(cb)); - } else if (handle !== undefined) { - debugStreamObj(this, '_final shutting down'); - const req = new ShutdownWrap(); - req.oncomplete = afterShutdown; - req.callback = cb; - req.handle = handle; - const err = handle.shutdown(req); - if (err === 1) // synchronous finish - return afterShutdown.call(req, 0); - } else { - cb(); + return; } + debugStreamObj(this, 'shutting down writable on _final'); + shutdownWritable.call(this, cb); } _read(nread) {