Skip to content

Commit 00972e4

Browse files
authored
perf: avoid intermediate promise on BodyReadable.dump (#4459)
* perf: avoid intermediate promise on `BodyReadable.dump` * also test stacktraces
1 parent 3567d3f commit 00972e4

File tree

2 files changed

+92
-7
lines changed

2 files changed

+92
-7
lines changed

lib/api/readable.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,24 +262,26 @@ class BodyReadable extends Readable {
262262
* @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
263263
* @returns {Promise<null>}
264264
*/
265-
async dump (opts) {
265+
dump (opts) {
266266
const signal = opts?.signal
267267

268268
if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
269-
throw new InvalidArgumentError('signal must be an AbortSignal')
269+
return Promise.reject(new InvalidArgumentError('signal must be an AbortSignal'))
270270
}
271271

272272
const limit = opts?.limit && Number.isFinite(opts.limit)
273273
? opts.limit
274274
: 128 * 1024
275275

276-
signal?.throwIfAborted()
276+
if (signal?.aborted) {
277+
return Promise.reject(signal.reason ?? new AbortError())
278+
}
277279

278280
if (this._readableState.closeEmitted) {
279-
return null
281+
return Promise.resolve(null)
280282
}
281283

282-
return await new Promise((resolve, reject) => {
284+
return new Promise((resolve, reject) => {
283285
if (
284286
(this[kContentLength] && (this[kContentLength] > limit)) ||
285287
this[kBytesRead] > limit

test/client-request.js

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { kConnect } = require('../lib/core/symbols')
1111
const { Readable } = require('node:stream')
1212
const net = require('node:net')
1313
const { promisify } = require('node:util')
14-
const { NotSupportedError, InvalidArgumentError } = require('../lib/core/errors')
14+
const { NotSupportedError, InvalidArgumentError, AbortError } = require('../lib/core/errors')
1515
const { parseFormDataString } = require('./utils/formdata')
1616

1717
test('request dump head', async (t) => {
@@ -116,7 +116,80 @@ test('request dump', async (t) => {
116116
})
117117

118118
test('request dump with abort signal', async (t) => {
119-
t = tspl(t, { plan: 2 })
119+
t = tspl(t, { plan: 10 })
120+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
121+
res.write('hello')
122+
})
123+
after(() => server.close())
124+
125+
server.listen(0, () => {
126+
const client = new Client(`http://localhost:${server.address().port}`)
127+
after(() => client.destroy())
128+
129+
client.request({
130+
path: '/',
131+
method: 'GET'
132+
}, (err, { body }) => {
133+
t.ifError(err)
134+
const ac = new AbortController()
135+
body.dump({ signal: ac.signal }).catch((err) => {
136+
t.strictEqual(err.name, 'AbortError')
137+
t.strictEqual(err.message, 'This operation was aborted')
138+
const stackLines = err.stack.split('\n').map((l) => l.trim())
139+
140+
t.ok(stackLines[0].startsWith('AbortError: This operation was aborted'))
141+
t.ok(stackLines[1].startsWith('at new DOMException'))
142+
t.ok(stackLines[2].startsWith('at AbortController.abort'))
143+
t.ok(/client-request.js/.test(stackLines[3]))
144+
t.ok(stackLines[4].startsWith('at RequestHandler.runInAsyncScope'))
145+
t.ok(stackLines[5].startsWith('at RequestHandler.onHeaders'))
146+
t.ok(stackLines[6].startsWith('at Request.onHeaders'))
147+
server.close()
148+
})
149+
ac.abort()
150+
})
151+
})
152+
153+
await t.completed
154+
})
155+
156+
test('request dump with POJO as invalid signal', async (t) => {
157+
t = tspl(t, { plan: 9 })
158+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
159+
res.write('hello')
160+
})
161+
after(() => server.close())
162+
163+
server.listen(0, () => {
164+
const client = new Client(`http://localhost:${server.address().port}`)
165+
after(() => client.destroy())
166+
167+
client.request({
168+
path: '/',
169+
method: 'GET'
170+
}, (err, { body }) => {
171+
t.ifError(err)
172+
body.dump({ signal: {} }).catch((err) => {
173+
t.strictEqual(err.name, 'InvalidArgumentError')
174+
t.strictEqual(err.message, 'signal must be an AbortSignal')
175+
const stackLines = err.stack.split('\n').map((l) => l.trim())
176+
177+
t.ok(stackLines[0].startsWith('InvalidArgumentError: signal must be an AbortSignal'))
178+
t.ok(stackLines[1].startsWith('at BodyReadable.dump'))
179+
t.ok(/client-request.js/.test(stackLines[2]))
180+
t.ok(stackLines[3].startsWith('at RequestHandler.runInAsyncScope'))
181+
t.ok(stackLines[4].startsWith('at RequestHandler.onHeaders'))
182+
t.ok(stackLines[5].startsWith('at Request.onHeaders'))
183+
server.close()
184+
})
185+
})
186+
})
187+
188+
await t.completed
189+
})
190+
191+
test('request dump with aborted signal', async (t) => {
192+
t = tspl(t, { plan: 8 })
120193
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
121194
res.write('hello')
122195
})
@@ -132,8 +205,18 @@ test('request dump with abort signal', async (t) => {
132205
}, (err, { body }) => {
133206
t.ifError(err)
134207
const ac = new AbortController()
208+
ac.abort(new AbortError('This operation was with purpose aborted'))
209+
135210
body.dump({ signal: ac.signal }).catch((err) => {
136211
t.strictEqual(err.name, 'AbortError')
212+
t.strictEqual(err.message, 'This operation was with purpose aborted')
213+
const stackLines = err.stack.split('\n').map((l) => l.trim())
214+
215+
t.ok(stackLines[0].startsWith('AbortError: This operation was with purpose aborted'))
216+
t.ok(/client-request.js/.test(stackLines[1]))
217+
t.ok(stackLines[2].startsWith('at RequestHandler.runInAsyncScope'))
218+
t.ok(stackLines[3].startsWith('at RequestHandler.onHeaders'))
219+
t.ok(stackLines[4].startsWith('at Request.onHeaders'))
137220
server.close()
138221
})
139222
ac.abort()

0 commit comments

Comments
 (0)