http2: fix abort when client.destroy inside end event

PR-URL: https://github.com/nodejs/node/pull/14239
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
James M Snell 2017-07-18 17:24:41 -07:00
parent 033c3b0a4d
commit 01a46f3981
8 changed files with 57 additions and 57 deletions

66
lib/internal/http2/core.js Normal file → Executable file
View File

@ -289,11 +289,9 @@ function onSessionRead(nread, buf, handle) {
_unrefActive(this); // Reset the session timeout timer _unrefActive(this); // Reset the session timeout timer
_unrefActive(stream); // Reset the stream timeout timer _unrefActive(stream); // Reset the stream timeout timer
if (nread >= 0) { if (nread >= 0 && !stream.destroyed) {
if (!stream.push(buf)) { if (!stream.push(buf)) {
assert(this.streamReadStop(id) === undefined, this.streamReadStop(id);
`HTTP/2 Stream ${id} does not exist. Please report this as ' +
'a bug in Node.js`);
state.reading = false; state.reading = false;
} }
} else { } else {
@ -1475,44 +1473,48 @@ class Http2Stream extends Duplex {
this.once('ready', this._destroy.bind(this, err, callback)); this.once('ready', this._destroy.bind(this, err, callback));
return; return;
} }
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`); process.nextTick(() => {
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
// Submit RST-STREAM frame if one hasn't been sent already and the // Submit RST-STREAM frame if one hasn't been sent already and the
// stream hasn't closed normally... // stream hasn't closed normally...
if (!this[kState].rst) { if (!this[kState].rst && !session.destroyed) {
const code = const code =
err instanceof Error ? err instanceof Error ?
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR; NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
this[kSession].rstStream(this, code); this[kSession].rstStream(this, code);
} }
// Remove the close handler on the session
session.removeListener('close', this[kState].closeHandler);
// Remove the close handler on the session // Unenroll the timer
session.removeListener('close', this[kState].closeHandler); unenroll(this);
// Unenroll the timer setImmediate(finishStreamDestroy.bind(this, handle));
unenroll(this);
setImmediate(finishStreamDestroy.bind(this, handle)); // All done
session[kState].streams.delete(this[kID]); const rst = this[kState].rst;
delete this[kSession]; const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
if (code !== NGHTTP2_NO_ERROR) {
// All done const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
const rst = this[kState].rst; process.nextTick(() => this.emit('error', err));
const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR; }
if (code !== NGHTTP2_NO_ERROR) { process.nextTick(emit.bind(this, 'streamClosed', code));
const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
process.nextTick(() => this.emit('error', err)); callback(err);
} });
process.nextTick(emit.bind(this, 'streamClosed', code));
debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
callback(err);
} }
} }
function finishStreamDestroy(handle) { function finishStreamDestroy(handle) {
const id = this[kID];
const session = this[kSession];
session[kState].streams.delete(id);
delete this[kSession];
if (handle !== undefined) if (handle !== undefined)
handle.destroyStream(this[kID]); handle.destroyStream(id);
this.emit('destroy');
} }
function processHeaders(headers) { function processHeaders(headers) {

0
src/node_http2.cc Normal file → Executable file
View File

6
src/node_http2.h Normal file → Executable file
View File

@ -329,7 +329,6 @@ class Http2Session : public AsyncWrap,
padding_strategy_ = opts.GetPaddingStrategy(); padding_strategy_ = opts.GetPaddingStrategy();
Init(env->event_loop(), type, *opts); Init(env->event_loop(), type, *opts);
stream_buf_.AllocateSufficientStorage(kAllocBufferSize);
} }
~Http2Session() override { ~Http2Session() override {
@ -456,7 +455,7 @@ class Http2Session : public AsyncWrap,
} }
char* stream_alloc() { char* stream_alloc() {
return *stream_buf_; return stream_buf_;
} }
private: private:
@ -464,7 +463,8 @@ class Http2Session : public AsyncWrap,
StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_; StreamResource::Callback<StreamResource::AllocCb> prev_alloc_cb_;
StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_; StreamResource::Callback<StreamResource::ReadCb> prev_read_cb_;
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
MaybeStackBuffer<char, kAllocBufferSize> stream_buf_;
char stream_buf_[kAllocBufferSize];
}; };
class ExternalHeader : class ExternalHeader :

7
src/node_http2_core-inl.h Normal file → Executable file
View File

@ -221,10 +221,7 @@ inline int Nghttp2Session::Free() {
Nghttp2Session* session = Nghttp2Session* session =
ContainerOf(&Nghttp2Session::prep_, ContainerOf(&Nghttp2Session::prep_,
reinterpret_cast<uv_prepare_t*>(handle)); reinterpret_cast<uv_prepare_t*>(handle));
session->OnFreeSession(); session->OnFreeSession();
DEBUG_HTTP2("Nghttp2Session %d: session is free\n",
session->session_type_);
}; };
uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose); uv_close(reinterpret_cast<uv_handle_t*>(&prep_), PrepClose);
@ -302,9 +299,9 @@ inline void Nghttp2Stream::ResetState(
inline void Nghttp2Stream::Destroy() { inline void Nghttp2Stream::Destroy() {
DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_);
// Do nothing if this stream instance is already destroyed // Do nothing if this stream instance is already destroyed
if (IsDestroyed() || IsDestroying()) if (IsDestroyed())
return; return;
flags_ |= NGHTTP2_STREAM_DESTROYING; flags_ |= NGHTTP2_STREAM_DESTROYED;
Nghttp2Session* session = this->session_; Nghttp2Session* session = this->session_;
if (session != nullptr) { if (session != nullptr) {

8
src/node_http2_core.h Normal file → Executable file
View File

@ -65,9 +65,7 @@ enum nghttp2_stream_flags {
// Stream is closed // Stream is closed
NGHTTP2_STREAM_CLOSED = 0x8, NGHTTP2_STREAM_CLOSED = 0x8,
// Stream is destroyed // Stream is destroyed
NGHTTP2_STREAM_DESTROYED = 0x10, NGHTTP2_STREAM_DESTROYED = 0x10
// Stream is being destroyed
NGHTTP2_STREAM_DESTROYING = 0x20
}; };
@ -290,10 +288,6 @@ class Nghttp2Stream {
return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED; return (flags_ & NGHTTP2_STREAM_DESTROYED) == NGHTTP2_STREAM_DESTROYED;
} }
inline bool IsDestroying() const {
return (flags_ & NGHTTP2_STREAM_DESTROYING) == NGHTTP2_STREAM_DESTROYING;
}
// Queue outbound chunks of data to be sent on this stream // Queue outbound chunks of data to be sent on this stream
inline int Write( inline int Write(
nghttp2_stream_write_t* req, nghttp2_stream_write_t* req,

View File

@ -51,6 +51,14 @@ server.on('listening', common.mustCall(() => {
const client = h2.connect(`http://localhost:${server.address().port}`, const client = h2.connect(`http://localhost:${server.address().port}`,
options); options);
let remaining = 2;
function maybeClose() {
if (--remaining === 0) {
server.close();
client.destroy();
}
}
const req = client.request({ ':path': '/' }); const req = client.request({ ':path': '/' });
// Because maxReservedRemoteStream is 1, the stream event // Because maxReservedRemoteStream is 1, the stream event
@ -59,15 +67,12 @@ server.on('listening', common.mustCall(() => {
client.on('stream', common.mustCall((stream) => { client.on('stream', common.mustCall((stream) => {
stream.resume(); stream.resume();
stream.on('end', common.mustCall()); stream.on('end', common.mustCall());
stream.on('streamClosed', common.mustCall(maybeClose));
})); }));
req.on('response', common.mustCall()); req.on('response', common.mustCall());
req.resume(); req.resume();
req.on('end', common.mustCall(() => { req.on('end', common.mustCall());
server.close(); req.on('streamClosed', common.mustCall(maybeClose));
client.destroy();
}));
req.end();
})); }));

3
test/parallel/test-http2-response-splitting.js Normal file → Executable file
View File

@ -65,7 +65,8 @@ server.listen(0, common.mustCall(() => {
assert.strictEqual(headers.location, undefined); assert.strictEqual(headers.location, undefined);
})); }));
req.resume(); req.resume();
req.on('end', common.mustCall(maybeClose)); req.on('end', common.mustCall());
req.on('streamClosed', common.mustCall(maybeClose));
} }
doTest(str, 'location', str); doTest(str, 'location', str);

7
test/parallel/test-http2-server-socket-destroy.js Normal file → Executable file
View File

@ -36,7 +36,9 @@ function onStream(stream) {
assert.notStrictEqual(stream.session, undefined); assert.notStrictEqual(stream.session, undefined);
socket.destroy(); socket.destroy();
assert.strictEqual(stream.session, undefined); stream.on('destroy', common.mustCall(() => {
assert.strictEqual(stream.session, undefined);
}));
} }
server.listen(0); server.listen(0);
@ -49,9 +51,8 @@ server.on('listening', common.mustCall(() => {
[HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST }); [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
req.on('aborted', common.mustCall()); req.on('aborted', common.mustCall());
req.resume();
req.on('end', common.mustCall()); req.on('end', common.mustCall());
req.on('response', common.mustCall());
req.on('data', common.mustCall());
client.on('close', common.mustCall()); client.on('close', common.mustCall());
})); }));