diff --git a/lib/request.js b/lib/request.js index 6f425d561..deeb577d5 100755 --- a/lib/request.js +++ b/lib/request.js @@ -24,6 +24,7 @@ exports = module.exports = internals.Request = class { constructor(server, req, res, options) { this._allowInternals = !!options.allowInternals; + this._closed = false; // true once the response has closed (esp. early) and will not emit any more events this._core = server._core; this._entity = null; // Entity information set via h.entity() this._eventContext = { request: this }; @@ -311,6 +312,7 @@ exports = module.exports = internals.Request = class { this.raw.req.on('close', internals.event.bind(this.raw.req, this._eventContext, 'close')); this.raw.req.on('error', internals.event.bind(this.raw.req, this._eventContext, 'error')); this.raw.req.on('aborted', internals.event.bind(this.raw.req, this._eventContext, 'abort')); + this.raw.res.once('close', internals.closed.bind(this.raw.res, this)); } _lookup() { @@ -687,6 +689,11 @@ internals.Info = class { }; +internals.closed = function (request) { + + request._closed = true; +}; + internals.event = function ({ request }, event, err) { if (!request) { diff --git a/lib/transmit.js b/lib/transmit.js index 195737f11..45a27ad25 100755 --- a/lib/transmit.js +++ b/lib/transmit.js @@ -244,6 +244,13 @@ internals.pipe = function (request, stream) { const env = { stream, request, team }; + if (request._closed) { + // No more events will be fired, so we proactively close-up shop + request.raw.res.end(); // Ensure res is finished so internals.end() doesn't think we're responding + internals.end(env, 'close'); + return team.work; + } + const aborted = internals.end.bind(null, env, 'aborted'); const close = internals.end.bind(null, env, 'close'); const end = internals.end.bind(null, env, null); diff --git a/test/transmit.js b/test/transmit.js index d77cc691e..0eb38ca0a 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -538,10 +538,17 @@ describe('transmission', () => { // Use state autoValue function to intercept marshal stage server.state('always', { - autoValue() { + async autoValue(request) { + const close = new Teamwork.Team(); + request.raw.res.once('close', () => close.attend()); + + // Will trigger abort then close. Prior to node v15.7.0 the res close came + // asynchronously after req abort, but since then it comes in the same tick. client.destroy(); - return team.work; // Continue once the request has been aborted + await close.work; + + return team.work; // Continue marshalling once the request has been aborted and response closed. } });