diff options
Diffstat (limited to 'node_modules/undici/lib/api')
| -rw-r--r-- | node_modules/undici/lib/api/abort-signal.js | 7 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/api-connect.js | 14 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/api-pipeline.js | 18 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/api-request.js | 84 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/api-stream.js | 22 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/api-upgrade.js | 19 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/readable.js | 229 | ||||
| -rw-r--r-- | node_modules/undici/lib/api/util.js | 97 |
8 files changed, 323 insertions, 167 deletions
diff --git a/node_modules/undici/lib/api/abort-signal.js b/node_modules/undici/lib/api/abort-signal.js index 2985c1e..2afe747 100644 --- a/node_modules/undici/lib/api/abort-signal.js +++ b/node_modules/undici/lib/api/abort-signal.js @@ -6,13 +6,16 @@ const kSignal = Symbol('kSignal') function abort (self) { if (self.abort) { - self.abort() + self.abort(self[kSignal]?.reason) } else { - self.onError(new RequestAbortedError()) + self.reason = self[kSignal]?.reason ?? new RequestAbortedError() } + removeSignal(self) } function addSignal (self, signal) { + self.reason = null + self[kSignal] = null self[kListener] = null diff --git a/node_modules/undici/lib/api/api-connect.js b/node_modules/undici/lib/api/api-connect.js index fd2b6ad..f9dbbf6 100644 --- a/node_modules/undici/lib/api/api-connect.js +++ b/node_modules/undici/lib/api/api-connect.js @@ -1,7 +1,8 @@ 'use strict' -const { AsyncResource } = require('async_hooks') -const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') +const assert = require('node:assert') +const { AsyncResource } = require('node:async_hooks') +const { InvalidArgumentError, SocketError } = require('../core/errors') const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') @@ -32,10 +33,13 @@ class ConnectHandler extends AsyncResource { } onConnect (abort, context) { - if (!this.callback) { - throw new RequestAbortedError() + if (this.reason) { + abort(this.reason) + return } + assert(this.callback) + this.abort = abort this.context = context } @@ -96,7 +100,7 @@ function connect (opts, callback) { if (typeof callback !== 'function') { throw err } - const opaque = opts && opts.opaque + const opaque = opts?.opaque queueMicrotask(() => callback(err, { opaque })) } } diff --git a/node_modules/undici/lib/api/api-pipeline.js b/node_modules/undici/lib/api/api-pipeline.js index af4a180..e64b329 100644 --- a/node_modules/undici/lib/api/api-pipeline.js +++ b/node_modules/undici/lib/api/api-pipeline.js @@ -4,16 +4,16 @@ const { Readable, Duplex, PassThrough -} = require('stream') +} = require('node:stream') const { InvalidArgumentError, InvalidReturnValueError, RequestAbortedError } = require('../core/errors') const util = require('../core/util') -const { AsyncResource } = require('async_hooks') +const { AsyncResource } = require('node:async_hooks') const { addSignal, removeSignal } = require('./abort-signal') -const assert = require('assert') +const assert = require('node:assert') const kResume = Symbol('resume') @@ -100,7 +100,7 @@ class PipelineHandler extends AsyncResource { read: () => { const { body } = this - if (body && body.resume) { + if (body?.resume) { body.resume() } }, @@ -147,12 +147,14 @@ class PipelineHandler extends AsyncResource { onConnect (abort, context) { const { ret, res } = this - assert(!res, 'pipeline cannot be retried') - - if (ret.destroyed) { - throw new RequestAbortedError() + if (this.reason) { + abort(this.reason) + return } + assert(!res, 'pipeline cannot be retried') + assert(!ret.destroyed) + this.abort = abort this.context = context } diff --git a/node_modules/undici/lib/api/api-request.js b/node_modules/undici/lib/api/api-request.js index d4281ce..be17d62 100644 --- a/node_modules/undici/lib/api/api-request.js +++ b/node_modules/undici/lib/api/api-request.js @@ -1,14 +1,11 @@ 'use strict' -const Readable = require('./readable') -const { - InvalidArgumentError, - RequestAbortedError -} = require('../core/errors') +const assert = require('node:assert') +const { Readable } = require('./readable') +const { InvalidArgumentError, RequestAbortedError } = require('../core/errors') const util = require('../core/util') const { getResolveErrorBodyCallback } = require('./util') -const { AsyncResource } = require('async_hooks') -const { addSignal, removeSignal } = require('./abort-signal') +const { AsyncResource } = require('node:async_hooks') class RequestHandler extends AsyncResource { constructor (opts, callback) { @@ -47,6 +44,7 @@ class RequestHandler extends AsyncResource { throw err } + this.method = method this.responseHeaders = responseHeaders || null this.opaque = opaque || null this.callback = callback @@ -58,6 +56,9 @@ class RequestHandler extends AsyncResource { this.onInfo = onInfo || null this.throwOnError = throwOnError this.highWaterMark = highWaterMark + this.signal = signal + this.reason = null + this.removeAbortListener = null if (util.isStream(body)) { body.on('error', (err) => { @@ -65,14 +66,36 @@ class RequestHandler extends AsyncResource { }) } - addSignal(this, signal) + if (this.signal) { + if (this.signal.aborted) { + this.reason = this.signal.reason ?? new RequestAbortedError() + } else { + this.removeAbortListener = util.addAbortListener(this.signal, () => { + this.reason = this.signal.reason ?? new RequestAbortedError() + if (this.res) { + util.destroy(this.res.on('error', util.nop), this.reason) + } else if (this.abort) { + this.abort(this.reason) + } + + if (this.removeAbortListener) { + this.res?.off('close', this.removeAbortListener) + this.removeAbortListener() + this.removeAbortListener = null + } + }) + } + } } onConnect (abort, context) { - if (!this.callback) { - throw new RequestAbortedError() + if (this.reason) { + abort(this.reason) + return } + assert(this.callback) + this.abort = abort this.context = context } @@ -91,14 +114,27 @@ class RequestHandler extends AsyncResource { const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers const contentType = parsedHeaders['content-type'] - const body = new Readable({ resume, abort, contentType, highWaterMark }) + const contentLength = parsedHeaders['content-length'] + const res = new Readable({ + resume, + abort, + contentType, + contentLength: this.method !== 'HEAD' && contentLength + ? Number(contentLength) + : null, + highWaterMark + }) + + if (this.removeAbortListener) { + res.on('close', this.removeAbortListener) + } this.callback = null - this.res = body + this.res = res if (callback !== null) { if (this.throwOnError && statusCode >= 400) { this.runInAsyncScope(getResolveErrorBodyCallback, null, - { callback, body, contentType, statusCode, statusMessage, headers } + { callback, body: res, contentType, statusCode, statusMessage, headers } ) } else { this.runInAsyncScope(callback, null, null, { @@ -106,7 +142,7 @@ class RequestHandler extends AsyncResource { headers, trailers: this.trailers, opaque, - body, + body: res, context }) } @@ -114,25 +150,17 @@ class RequestHandler extends AsyncResource { } onData (chunk) { - const { res } = this - return res.push(chunk) + return this.res.push(chunk) } onComplete (trailers) { - const { res } = this - - removeSignal(this) - util.parseHeaders(trailers, this.trailers) - - res.push(null) + this.res.push(null) } onError (err) { const { res, callback, body, opaque } = this - removeSignal(this) - if (callback) { // TODO: Does this need queueMicrotask? this.callback = null @@ -153,6 +181,12 @@ class RequestHandler extends AsyncResource { this.body = null util.destroy(body, err) } + + if (this.removeAbortListener) { + res?.off('close', this.removeAbortListener) + this.removeAbortListener() + this.removeAbortListener = null + } } } @@ -171,7 +205,7 @@ function request (opts, callback) { if (typeof callback !== 'function') { throw err } - const opaque = opts && opts.opaque + const opaque = opts?.opaque queueMicrotask(() => callback(err, { opaque })) } } diff --git a/node_modules/undici/lib/api/api-stream.js b/node_modules/undici/lib/api/api-stream.js index c571a6f..fba2266 100644 --- a/node_modules/undici/lib/api/api-stream.js +++ b/node_modules/undici/lib/api/api-stream.js @@ -1,14 +1,11 @@ 'use strict' -const { finished, PassThrough } = require('stream') -const { - InvalidArgumentError, - InvalidReturnValueError, - RequestAbortedError -} = require('../core/errors') +const assert = require('node:assert') +const { finished, PassThrough } = require('node:stream') +const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors') const util = require('../core/util') const { getResolveErrorBodyCallback } = require('./util') -const { AsyncResource } = require('async_hooks') +const { AsyncResource } = require('node:async_hooks') const { addSignal, removeSignal } = require('./abort-signal') class StreamHandler extends AsyncResource { @@ -70,10 +67,13 @@ class StreamHandler extends AsyncResource { } onConnect (abort, context) { - if (!this.callback) { - throw new RequestAbortedError() + if (this.reason) { + abort(this.reason) + return } + assert(this.callback) + this.abort = abort this.context = context } @@ -148,7 +148,7 @@ class StreamHandler extends AsyncResource { const needDrain = res.writableNeedDrain !== undefined ? res.writableNeedDrain - : res._writableState && res._writableState.needDrain + : res._writableState?.needDrain return needDrain !== true } @@ -212,7 +212,7 @@ function stream (opts, factory, callback) { if (typeof callback !== 'function') { throw err } - const opaque = opts && opts.opaque + const opaque = opts?.opaque queueMicrotask(() => callback(err, { opaque })) } } diff --git a/node_modules/undici/lib/api/api-upgrade.js b/node_modules/undici/lib/api/api-upgrade.js index ef783e8..7effcf2 100644 --- a/node_modules/undici/lib/api/api-upgrade.js +++ b/node_modules/undici/lib/api/api-upgrade.js @@ -1,10 +1,10 @@ 'use strict' -const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') -const { AsyncResource } = require('async_hooks') +const { InvalidArgumentError, SocketError } = require('../core/errors') +const { AsyncResource } = require('node:async_hooks') const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') -const assert = require('assert') +const assert = require('node:assert') class UpgradeHandler extends AsyncResource { constructor (opts, callback) { @@ -34,10 +34,13 @@ class UpgradeHandler extends AsyncResource { } onConnect (abort, context) { - if (!this.callback) { - throw new RequestAbortedError() + if (this.reason) { + abort(this.reason) + return } + assert(this.callback) + this.abort = abort this.context = null } @@ -47,9 +50,9 @@ class UpgradeHandler extends AsyncResource { } onUpgrade (statusCode, rawHeaders, socket) { - const { callback, opaque, context } = this + assert(statusCode === 101) - assert.strictEqual(statusCode, 101) + const { callback, opaque, context } = this removeSignal(this) @@ -97,7 +100,7 @@ function upgrade (opts, callback) { if (typeof callback !== 'function') { throw err } - const opaque = opts && opts.opaque + const opaque = opts?.opaque queueMicrotask(() => callback(err, { opaque })) } } diff --git a/node_modules/undici/lib/api/readable.js b/node_modules/undici/lib/api/readable.js index 5269dfa..47fbf3e 100644 --- a/node_modules/undici/lib/api/readable.js +++ b/node_modules/undici/lib/api/readable.js @@ -2,27 +2,27 @@ 'use strict' -const assert = require('assert') -const { Readable } = require('stream') -const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors') +const assert = require('node:assert') +const { Readable } = require('node:stream') +const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors') const util = require('../core/util') -const { ReadableStreamFrom, toUSVString } = require('../core/util') - -let Blob +const { ReadableStreamFrom } = require('../core/util') const kConsume = Symbol('kConsume') const kReading = Symbol('kReading') const kBody = Symbol('kBody') -const kAbort = Symbol('abort') +const kAbort = Symbol('kAbort') const kContentType = Symbol('kContentType') +const kContentLength = Symbol('kContentLength') const noop = () => {} -module.exports = class BodyReadable extends Readable { +class BodyReadable extends Readable { constructor ({ resume, abort, contentType = '', + contentLength, highWaterMark = 64 * 1024 // Same as nodejs fs streams. }) { super({ @@ -37,6 +37,7 @@ module.exports = class BodyReadable extends Readable { this[kConsume] = null this[kBody] = null this[kContentType] = contentType + this[kContentLength] = contentLength // Is stream being consumed through Readable API? // This is an optimization so that we avoid checking @@ -46,11 +47,6 @@ module.exports = class BodyReadable extends Readable { } destroy (err) { - if (this.destroyed) { - // Node < 16 - return this - } - if (!err && !this._readableState.endEmitted) { err = new RequestAbortedError() } @@ -62,15 +58,18 @@ module.exports = class BodyReadable extends Readable { return super.destroy(err) } - emit (ev, ...args) { - if (ev === 'data') { - // Node < 16.7 - this._readableState.dataEmitted = true - } else if (ev === 'error') { - // Node < 16 - this._readableState.errorEmitted = true + _destroy (err, callback) { + // Workaround for Node "bug". If the stream is destroyed in same + // tick as it is created, then a user who is waiting for a + // promise (i.e micro tick) for installing a 'error' listener will + // never get a chance and will always encounter an unhandled exception. + if (!this[kReading]) { + setImmediate(() => { + callback(err) + }) + } else { + callback(err) } - return super.emit(ev, ...args) } on (ev, ...args) { @@ -100,7 +99,7 @@ module.exports = class BodyReadable extends Readable { } push (chunk) { - if (this[kConsume] && chunk !== null && this.readableLength === 0) { + if (this[kConsume] && chunk !== null) { consumePush(this[kConsume], chunk) return this[kReading] ? super.push(chunk) : true } @@ -122,6 +121,11 @@ module.exports = class BodyReadable extends Readable { return consume(this, 'blob') } + // https://fetch.spec.whatwg.org/#dom-body-bytes + async bytes () { + return consume(this, 'bytes') + } + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer async arrayBuffer () { return consume(this, 'arrayBuffer') @@ -151,37 +155,35 @@ module.exports = class BodyReadable extends Readable { return this[kBody] } - dump (opts) { - let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 - const signal = opts && opts.signal + async dump (opts) { + let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024 + const signal = opts?.signal - if (signal) { - try { - if (typeof signal !== 'object' || !('aborted' in signal)) { - throw new InvalidArgumentError('signal must be an AbortSignal') - } - util.throwIfAborted(signal) - } catch (err) { - return Promise.reject(err) - } + if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) { + throw new InvalidArgumentError('signal must be an AbortSignal') } - if (this.closed) { - return Promise.resolve(null) + signal?.throwIfAborted() + + if (this._readableState.closeEmitted) { + return null } - return new Promise((resolve, reject) => { - const signalListenerCleanup = signal - ? util.addAbortListener(signal, () => { - this.destroy() - }) - : noop + return await new Promise((resolve, reject) => { + if (this[kContentLength] > limit) { + this.destroy(new AbortError()) + } + + const onAbort = () => { + this.destroy(signal.reason ?? new AbortError()) + } + signal?.addEventListener('abort', onAbort) this .on('close', function () { - signalListenerCleanup() - if (signal && signal.aborted) { - reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + signal?.removeEventListener('abort', onAbort) + if (signal?.aborted) { + reject(signal.reason ?? new AbortError()) } else { resolve(null) } @@ -210,33 +212,46 @@ function isUnusable (self) { } async function consume (stream, type) { - if (isUnusable(stream)) { - throw new TypeError('unusable') - } - assert(!stream[kConsume]) return new Promise((resolve, reject) => { - stream[kConsume] = { - type, - stream, - resolve, - reject, - length: 0, - body: [] - } - - stream - .on('error', function (err) { - consumeFinish(this[kConsume], err) - }) - .on('close', function () { - if (this[kConsume].body !== null) { - consumeFinish(this[kConsume], new RequestAbortedError()) + if (isUnusable(stream)) { + const rState = stream._readableState + if (rState.destroyed && rState.closeEmitted === false) { + stream + .on('error', err => { + reject(err) + }) + .on('close', () => { + reject(new TypeError('unusable')) + }) + } else { + reject(rState.errored ?? new TypeError('unusable')) + } + } else { + queueMicrotask(() => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] } - }) - process.nextTick(consumeStart, stream[kConsume]) + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + consumeStart(stream[kConsume]) + }) + } }) } @@ -247,8 +262,16 @@ function consumeStart (consume) { const { _readableState: state } = consume.stream - for (const chunk of state.buffer) { - consumePush(consume, chunk) + if (state.bufferIndex) { + const start = state.bufferIndex + const end = state.buffer.length + for (let n = start; n < end; n++) { + consumePush(consume, state.buffer[n]) + } + } else { + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } } if (state.endEmitted) { @@ -266,29 +289,67 @@ function consumeStart (consume) { } } +/** + * @param {Buffer[]} chunks + * @param {number} length + */ +function chunksDecode (chunks, length) { + if (chunks.length === 0 || length === 0) { + return '' + } + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length) + const bufferLength = buffer.length + + // Skip BOM. + const start = + bufferLength > 2 && + buffer[0] === 0xef && + buffer[1] === 0xbb && + buffer[2] === 0xbf + ? 3 + : 0 + return buffer.utf8Slice(start, bufferLength) +} + +/** + * @param {Buffer[]} chunks + * @param {number} length + * @returns {Uint8Array} + */ +function chunksConcat (chunks, length) { + if (chunks.length === 0 || length === 0) { + return new Uint8Array(0) + } + if (chunks.length === 1) { + // fast-path + return new Uint8Array(chunks[0]) + } + const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer) + + let offset = 0 + for (let i = 0; i < chunks.length; ++i) { + const chunk = chunks[i] + buffer.set(chunk, offset) + offset += chunk.length + } + + return buffer +} + function consumeEnd (consume) { const { type, body, resolve, stream, length } = consume try { if (type === 'text') { - resolve(toUSVString(Buffer.concat(body))) + resolve(chunksDecode(body, length)) } else if (type === 'json') { - resolve(JSON.parse(Buffer.concat(body))) + resolve(JSON.parse(chunksDecode(body, length))) } else if (type === 'arrayBuffer') { - const dst = new Uint8Array(length) - - let pos = 0 - for (const buf of body) { - dst.set(buf, pos) - pos += buf.byteLength - } - - resolve(dst.buffer) + resolve(chunksConcat(body, length).buffer) } else if (type === 'blob') { - if (!Blob) { - Blob = require('buffer').Blob - } resolve(new Blob(body, { type: stream[kContentType] })) + } else if (type === 'bytes') { + resolve(chunksConcat(body, length)) } consumeFinish(consume) @@ -320,3 +381,5 @@ function consumeFinish (consume, err) { consume.length = 0 consume.body = null } + +module.exports = { Readable: BodyReadable, chunksDecode } diff --git a/node_modules/undici/lib/api/util.js b/node_modules/undici/lib/api/util.js index bffd702..2f983bf 100644 --- a/node_modules/undici/lib/api/util.js +++ b/node_modules/undici/lib/api/util.js @@ -1,46 +1,93 @@ -const assert = require('assert') +const assert = require('node:assert') const { ResponseStatusCodeError } = require('../core/errors') -const { toUSVString } = require('../core/util') + +const { chunksDecode } = require('./readable') +const CHUNK_LIMIT = 128 * 1024 async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { assert(body) let chunks = [] - let limit = 0 + let length = 0 - for await (const chunk of body) { - chunks.push(chunk) - limit += chunk.length - if (limit > 128 * 1024) { - chunks = null - break + try { + for await (const chunk of body) { + chunks.push(chunk) + length += chunk.length + if (length > CHUNK_LIMIT) { + chunks = [] + length = 0 + break + } } + } catch { + chunks = [] + length = 0 + // Do nothing.... } - if (statusCode === 204 || !contentType || !chunks) { - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}` + + if (statusCode === 204 || !contentType || !length) { + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers))) return } - try { - if (contentType.startsWith('application/json')) { - const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return - } + const stackTraceLimit = Error.stackTraceLimit + Error.stackTraceLimit = 0 + let payload - if (contentType.startsWith('text/')) { - const payload = toUSVString(Buffer.concat(chunks)) - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return + try { + if (isContentTypeApplicationJson(contentType)) { + payload = JSON.parse(chunksDecode(chunks, length)) + } else if (isContentTypeText(contentType)) { + payload = chunksDecode(chunks, length) } - } catch (err) { - // Process in a fallback if error + } catch { + // process in a callback to avoid throwing in the microtask queue + } finally { + Error.stackTraceLimit = stackTraceLimit } + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload))) +} - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +const isContentTypeApplicationJson = (contentType) => { + return ( + contentType.length > 15 && + contentType[11] === '/' && + contentType[0] === 'a' && + contentType[1] === 'p' && + contentType[2] === 'p' && + contentType[3] === 'l' && + contentType[4] === 'i' && + contentType[5] === 'c' && + contentType[6] === 'a' && + contentType[7] === 't' && + contentType[8] === 'i' && + contentType[9] === 'o' && + contentType[10] === 'n' && + contentType[12] === 'j' && + contentType[13] === 's' && + contentType[14] === 'o' && + contentType[15] === 'n' + ) } -module.exports = { getResolveErrorBodyCallback } +const isContentTypeText = (contentType) => { + return ( + contentType.length > 4 && + contentType[4] === '/' && + contentType[0] === 't' && + contentType[1] === 'e' && + contentType[2] === 'x' && + contentType[3] === 't' + ) +} + +module.exports = { + getResolveErrorBodyCallback, + isContentTypeApplicationJson, + isContentTypeText +} |
