From 65eea9be3d147c8957e17ede3848c5ae409ca23e Mon Sep 17 00:00:00 2001 From: Debadree Chatterjee Date: Fri, 10 Mar 2023 16:15:18 +0530 Subject: [PATCH] feat: add abort signal to body.dump() (#1993) --- lib/api/readable.js | 20 ++++++++++++++++++-- lib/core/util.js | 15 +++++++++++++++ test/client-request.js | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/lib/api/readable.js b/lib/api/readable.js index 9c184d14e1c..a184e8eb51b 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -4,7 +4,7 @@ const assert = require('assert') const { Readable } = require('stream') -const { RequestAbortedError, NotSupportedError } = require('../core/errors') +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors') const util = require('../core/util') const { ReadableStreamFrom, toUSVString } = require('../core/util') @@ -146,15 +146,31 @@ module.exports = class BodyReadable extends Readable { async dump (opts) { let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + const abortFn = () => { + this.destroy() + } + if (signal) { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + signal.addEventListener('abort', abortFn, { once: true }) + } try { for await (const chunk of this) { + util.throwIfAborted(signal) limit -= Buffer.byteLength(chunk) if (limit < 0) { return } } } catch { - // Do nothing... + util.throwIfAborted(signal) + } finally { + if (signal) { + signal.removeEventListener('abort', abortFn) + } } } } diff --git a/lib/core/util.js b/lib/core/util.js index e203919cc35..ab94bcfe51c 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -395,6 +395,20 @@ function isFormDataLike (object) { ) } +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + const kEnumerableProperty = Object.create(null) kEnumerableProperty.enumerable = true @@ -426,6 +440,7 @@ module.exports = { getSocketInfo, isFormDataLike, buildURL, + throwIfAborted, nodeMajor, nodeMinor, nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) diff --git a/test/client-request.js b/test/client-request.js index ff564604d42..2d73e9892ee 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -1,3 +1,5 @@ +/* globals AbortController */ + 'use strict' const { test } = require('tap') @@ -41,6 +43,38 @@ test('request dump', (t) => { }) }) +test('request dump with abort signal', (t) => { + t.plan(2) + const server = createServer((req, res) => { + res.write('hello') + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET' + }, (err, { body }) => { + t.error(err) + let ac + if (!global.AbortController) { + const { AbortController } = require('abort-controller') + ac = new AbortController() + } else { + ac = new AbortController() + } + body.dump({ signal: ac.signal }).catch((err) => { + t.equal(err.name, 'AbortError') + server.close() + }) + ac.abort() + }) + }) +}) + test('request abort before headers', (t) => { t.plan(6)