Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const {
kSocket,
kRequest,
kProxySocket,
kIncomplete,
assertValidPseudoHeader,
getAuthority,
} = require('internal/http2/util');
Expand Down Expand Up @@ -291,15 +292,24 @@ function onStreamCloseRequest() {
const state = req[kState];
state.closed = true;

this[kProxySocket] = null;
this[kRequest] = undefined;

// The peer closed the stream before END_STREAM, so the request body is
// incomplete. Like http1's IncomingMessage on abort, destroy the request
// instead of ending it, so 'end' is not emitted for a truncated body.
// destroy() emits 'close'.
if (this[kIncomplete]) {
req.destroy();
return;
}

req.push(null);
// If the user didn't interact with incoming data and didn't pipe it,
// dump it for compatibility with http1
if (!state.didRead && !req._readableState.resumeScheduled)
req.resume();

this[kProxySocket] = null;
this[kRequest] = undefined;

req.emit('close');
}

Expand Down
4 changes: 4 additions & 0 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const {
kStrictSingleValueFields,
kSocket,
kRequest,
kIncomplete,
kProtocol,
kProxySocket,
MAX_ADDITIONAL_SETTINGS,
Expand Down Expand Up @@ -652,6 +653,9 @@ function onStreamClose(code, peerReset) {
const cleanCode = code === NGHTTP2_NO_ERROR || code === NGHTTP2_CANCEL;

if (!peerReadableEnded) {
// The peer closed the stream before END_STREAM, so the incoming message
// is incomplete. destroy() ends the readable side, so record it here.
stream[kIncomplete] = true;
stream.destroy(cleanCode ?
new ERR_HTTP2_STREAM_ABORTED() :
new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code));
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const kSocket = Symbol('socket');
const kProtocol = Symbol('protocol');
const kProxySocket = Symbol('proxySocket');
const kRequest = Symbol('request');
const kIncomplete = Symbol('incomplete');

const {
NGHTTP2_NV_FLAG_NONE,
Expand Down Expand Up @@ -1005,6 +1006,7 @@ module.exports = {
kStrictSingleValueFields,
kSocket,
kProtocol,
kIncomplete,
kProxySocket,
kRequest,
MAX_ADDITIONAL_SETTINGS,
Expand Down
83 changes: 83 additions & 0 deletions test/parallel/test-http2-compat-serverrequest-end-after-abort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

// A request whose stream is reset by the peer before END_STREAM has an
// incomplete body. Like http1, the compat request must not emit 'end' for it.

const assert = require('assert');
const http2 = require('http2');
const { finished } = require('stream');

const codes = [
http2.constants.NGHTTP2_NO_ERROR,
http2.constants.NGHTTP2_CANCEL,
];

const server = http2.createServer(common.mustCall((req, res) => {
// Tell the client the partial body arrived, so it can reset the stream.
req.once('data', common.mustCall(() => res.writeHead(200).flushHeaders()));
req.on('end', common.mustNotCall());
req.on('aborted', common.mustCall());
req.on('error', common.expectsError({ code: 'ERR_HTTP2_STREAM_ABORTED' }));
req.on('close', common.mustCall(() => {
assert.strictEqual(req.aborted, true);
assert.strictEqual(req.readableEnded, false);
}));
finished(req, common.mustCall((err) => {
assert.ok(err);
}));
}, codes.length));

server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
let pending = codes.length;

for (const code of codes) {
const req = client.request({
':method': 'POST',
'content-length': '1000',
});
req.on('error', () => {});
req.on('close', common.mustCall(() => {
if (--pending === 0) {
client.close();
server.close();
}
}));
// Send part of the body, then reset the stream without END_STREAM once
// the server has seen it.
req.write('{"action":"partial"}');
req.on('response', common.mustCall(() => req.close(code)));
}
}));

// A complete body followed by a reset still ends the request normally.
{
const server = http2.createServer(common.mustCall((req, res) => {
let body = '';
req.setEncoding('utf8');
req.on('data', (chunk) => body += chunk);
req.on('end', common.mustCall(() => {
assert.strictEqual(body, 'complete');
res.writeHead(200).flushHeaders();
}));
req.on('error', common.mustNotCall());
}));

server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`);
const req = client.request({ ':method': 'POST' });
req.on('error', () => {});
req.on('response', common.mustCall(() => {
req.close(http2.constants.NGHTTP2_CANCEL);
}));
req.on('close', common.mustCall(() => {
client.close();
server.close();
}));
req.end('complete');
}));
}
Loading