diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 04b13358ca717f..18aed3b8df037d 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -29,12 +29,15 @@ const { const { Readable, finished } = require('stream'); +const { AbortController } = require('internal/abort_controller'); + const kHeaders = Symbol('kHeaders'); const kHeadersDistinct = Symbol('kHeadersDistinct'); const kHeadersCount = Symbol('kHeadersCount'); const kTrailers = Symbol('kTrailers'); const kTrailersDistinct = Symbol('kTrailersDistinct'); const kTrailersCount = Symbol('kTrailersCount'); +const kAbortController = Symbol('kAbortController'); function readStart(socket) { if (socket && !socket._paused && socket.readable) @@ -90,6 +93,7 @@ function IncomingMessage(socket) { // Flag for when we decide that this message cannot possibly be // read by the user, so there's no point continuing to handle it. this._dumped = false; + this[kAbortController] = null; } ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype); ObjectSetPrototypeOf(IncomingMessage, Readable); @@ -184,6 +188,25 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { }, }); +ObjectDefineProperty(IncomingMessage.prototype, 'signal', { + __proto__: null, + configurable: true, + get: function() { + if (this[kAbortController] === null) { + const ac = new AbortController(); + this[kAbortController] = ac; + if (this.destroyed) { + ac.abort(); + } else { + this.once('close', function() { + ac.abort(); + }); + } + } + return this[kAbortController].signal; + }, +}); + IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { if (callback) this.on('timeout', callback); diff --git a/test/parallel/test-http-request-signal.js b/test/parallel/test-http-request-signal.js new file mode 100644 index 00000000000000..b7c9fdc79e8365 --- /dev/null +++ b/test/parallel/test-http-request-signal.js @@ -0,0 +1,85 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +// Test 1: req.signal is an AbortSignal and aborts on 'close' +{ + const server = http.createServer(common.mustCall((req, res) => { + assert.ok(req.signal instanceof AbortSignal); + assert.strictEqual(req.signal.aborted, false); + req.signal.onabort = common.mustCall(() => { + assert.strictEqual(req.signal.aborted, true); + }); + res.destroy(); + })); + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, () => {}).on('error', () => { + server.close(); + }); + })); +} + +// Test 2: req.signal is aborted if accessed after destroy +{ + const req = new http.IncomingMessage(null); + req.destroy(); + assert.strictEqual(req.signal.aborted, true); +} + +// Test 3: Multiple accesses return the same signal +{ + const req = new http.IncomingMessage(null); + assert.strictEqual(req.signal, req.signal); +} + + +// Test 4: res.signal on a client-side http.request() response (IncomingMessage). +{ + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200); + res.write('partial'); + })); + + server.listen(0, common.mustCall(() => { + const clientReq = http.request( + { port: server.address().port }, + common.mustCall((res) => { + assert.ok(res.signal instanceof AbortSignal); + assert.strictEqual(res.signal.aborted, false); + + res.signal.onabort = common.mustCall(() => { + assert.strictEqual(res.signal.aborted, true); + server.close(); + }); + clientReq.destroy(); + }), + ); + clientReq.on('error', () => {}); + clientReq.end(); + })); +} + +// Test 5: Client cancels a pending request. +{ + const server = http.createServer(common.mustCall((req, res) => { + req.signal.onabort = common.mustCall(() => { + assert.strictEqual(req.signal.aborted, true); + server.close(); + }); + res.flushHeaders(); + })); + + server.listen(0, common.mustCall(() => { + const clientReq = http.request( + { port: server.address().port }, + common.mustCall((res) => { + res.on('error', () => {}); + clientReq.destroy(); + }), + ); + clientReq.on('error', () => {}); + clientReq.end(); + })); +}