diff --git a/lib/common.js b/lib/common.js index f77941728..ad059fae9 100644 --- a/lib/common.js +++ b/lib/common.js @@ -673,6 +673,34 @@ function removeAllTimers() { clearTimer(clearImmediate, immediates) } +/** + * Check if the Client Request has been cancelled. + * + * Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled. + * The two flags have the same purpose, but the Node maintainers are migrating from `abort(ed)` to + * `destroy(ed)` terminology, to be more consistent with `stream.Writable`. + * In Node 14.x+, Calling `abort()` will set both `aborted` and `destroyed` to true, however, + * calling `destroy()` will only set `destroyed` to true. + * Falling back on checking if the socket is destroyed to cover the case of Node <14.x where + * `destroy()` is called, but `destroyed` is undefined. + * + * Node Client Request history: + * - `request.abort()`: Added in: v0.3.8, Deprecated since: v14.1.0, v13.14.0 + * - `request.aborted`: Added in: v0.11.14, Became a boolean instead of a timestamp: v11.0.0, Not deprecated (yet) + * - `request.destroy()`: Added in: v0.3.0 + * - `request.destroyed`: Added in: v14.1.0, v13.14.0 + * + * @param {ClientRequest} req + * @returns {boolean} + */ +function isRequestDestroyed(req) { + return !!( + req.destroyed === true || + req.aborted || + (req.socket && req.socket.destroyed) + ) +} + module.exports = { contentEncoding, dataEqual, @@ -686,6 +714,7 @@ module.exports = { isContentEncoded, isJSONContent, isPlainObject, + isRequestDestroyed, isStream, isUtf8Representable, mapValue, diff --git a/lib/intercepted_request_router.js b/lib/intercepted_request_router.js index 0e7417bfb..626fea865 100644 --- a/lib/intercepted_request_router.js +++ b/lib/intercepted_request_router.js @@ -103,8 +103,7 @@ class InterceptedRequestRouter { connectSocket() { const { req, socket } = this - // Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled. - if (req.destroyed || req.aborted) { + if (common.isRequestDestroyed(req)) { return } @@ -250,8 +249,7 @@ class InterceptedRequestRouter { return } - // Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled. - if (!req.destroyed && !req.aborted && !playbackStarted) { + if (!common.isRequestDestroyed(req) && !playbackStarted) { this.startPlayback() } } diff --git a/lib/playback_interceptor.js b/lib/playback_interceptor.js index 142e30442..08ebf45b4 100644 --- a/lib/playback_interceptor.js +++ b/lib/playback_interceptor.js @@ -293,7 +293,7 @@ function playbackInterceptor({ const { delayBodyInMs, delayConnectionInMs } = interceptor function respond() { - if (req.aborted) { + if (common.isRequestDestroyed(req)) { return } @@ -318,7 +318,7 @@ function playbackInterceptor({ // correct events are emitted first ('socket', 'finish') and any aborts in the in the queue or // called during a 'finish' listener can be called. common.setImmediate(() => { - if (!req.aborted) { + if (!common.isRequestDestroyed(req)) { start() } }) diff --git a/tests/test_destroy.js b/tests/test_destroy.js index b21b1cce6..de6948e95 100644 --- a/tests/test_destroy.js +++ b/tests/test_destroy.js @@ -34,4 +34,20 @@ describe('`res.destroy()`', () => { expect.fail('should not emit error') }) }) + + it('should not emit an response if destroyed first', done => { + nock('http://example.test').get('/').reply() + + const req = http + .get('http://example.test/', () => { + expect.fail('should not emit a response') + }) + .on('error', () => {}) // listen for error so "socket hang up" doesn't bubble + .on('socket', () => { + setImmediate(() => req.destroy()) + }) + + // give the `setImmediate` calls enough time to cycle. + setTimeout(() => done(), 10) + }) }) diff --git a/tests/test_nock_off.js b/tests/test_nock_off.js index 678bd2d36..1e0aa7f04 100644 --- a/tests/test_nock_off.js +++ b/tests/test_nock_off.js @@ -30,7 +30,9 @@ describe('NOCK_OFF env var', () => { .get('/') .reply(200, 'mock') - const { body } = await got(origin, { ca: httpsServer.ca }) + const { body } = await got(origin, { + https: { certificateAuthority: httpsServer.ca }, + }) expect(body).to.equal(responseBody) scope.done() })