Update Undici to 6.29.1 (#1308)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno BorgesandCopilot App authored and GitHub committed 2026-10-30 12:52:17 -04:00
1 parent 816722acf8
commit 8cd57eb681
4 files changed
+272 -64

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: undici name: undici
version: 6.29.0 version: 6.29.1
type: npm type: npm
summary: An HTTP/1.1 client, written from scratch for Node.js summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org homepage: https://undici.nodejs.org
+134 -30
View File
@@ -7217,7 +7217,7 @@ function defaultFactory (origin, opts) {
} }
class Agent extends DispatcherBase { class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) {
if (typeof factory !== 'function') { if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.') throw new InvalidArgumentError('factory must be a function.')
} }
@@ -7236,11 +7236,15 @@ class Agent extends DispatcherBase {
connect = { ...connect } connect = { ...connect }
} }
if (tls && typeof tls !== 'function') {
tls = { ...tls }
}
this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent)
? options.interceptors.Agent ? options.interceptors.Agent
: [createRedirectInterceptor({ maxRedirections })] : [createRedirectInterceptor({ maxRedirections })]
this[kOptions] = { ...util.deepClone(options), connect } this[kOptions] = { ...util.deepClone(options), connect, tls }
this[kOptions].interceptors = options.interceptors this[kOptions].interceptors = options.interceptors
? { ...options.interceptors } ? { ...options.interceptors }
: undefined : undefined
@@ -8273,8 +8277,6 @@ async function connectH1 (client, socket) {
socket[kParser] = new Parser(client, socket, llhttpInstance) socket[kParser] = new Parser(client, socket, llhttpInstance)
addListener(socket, 'error', function (err) { addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
const parser = this[kParser] const parser = this[kParser]
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
@@ -9204,8 +9206,6 @@ async function connectH2 (client, socket) {
socket[kHTTP2Session] = session socket[kHTTP2Session] = session
util.addListener(socket, 'error', function (err) { util.addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kError] = err this[kError] = err
this[kClient][kOnError](err) this[kClient][kOnError](err)
@@ -9279,8 +9279,6 @@ function resumeH2 (client) {
} }
function onHttp2SessionError (err) { function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kSocket][kError] = err this[kSocket][kError] = err
this[kClient][kOnError](err) this[kClient][kOnError](err)
} }
@@ -9978,9 +9976,10 @@ class Client extends DispatcherBase {
// h2 // h2
maxConcurrentStreams, maxConcurrentStreams,
allowH2, allowH2,
webSocket webSocket,
eventSource
} = {}) { } = {}) {
super({ webSocket }) super({ webSocket, eventSource })
if (keepAlive !== undefined) { if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
@@ -10501,6 +10500,7 @@ module.exports = Client
const buffer = __nccwpck_require__(4573)
const Dispatcher = __nccwpck_require__(883) const Dispatcher = __nccwpck_require__(883)
const { const {
ClientDestroyedError, ClientDestroyedError,
@@ -10513,6 +10513,7 @@ const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed') const kOnClosed = Symbol('onClosed')
const kInterceptedDispatch = Symbol('Intercepted Dispatch') const kInterceptedDispatch = Symbol('Intercepted Dispatch')
const kWebSocketOptions = Symbol('webSocketOptions') const kWebSocketOptions = Symbol('webSocketOptions')
const kEventSourceOptions = Symbol('eventSourceOptions')
class DispatcherBase extends Dispatcher { class DispatcherBase extends Dispatcher {
constructor (opts) { constructor (opts) {
@@ -10523,6 +10524,7 @@ class DispatcherBase extends Dispatcher {
this[kClosed] = false this[kClosed] = false
this[kOnClosed] = [] this[kOnClosed] = []
this[kWebSocketOptions] = opts?.webSocket ?? {} this[kWebSocketOptions] = opts?.webSocket ?? {}
this[kEventSourceOptions] = opts?.eventSource ?? {}
} }
get webSocketOptions () { get webSocketOptions () {
@@ -10532,6 +10534,12 @@ class DispatcherBase extends Dispatcher {
} }
} }
get eventSourceOptions () {
return {
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
}
}
get destroyed () { get destroyed () {
return this[kDestroyed] return this[kDestroyed]
} }
@@ -16890,6 +16898,7 @@ module.exports = {
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const buffer = __nccwpck_require__(4573)
const { Transform } = __nccwpck_require__(7075) const { Transform } = __nccwpck_require__(7075)
const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(4811) const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(4811)
@@ -16914,11 +16923,19 @@ const COLON = 0x3A
*/ */
const SPACE = 0x20 const SPACE = 0x20
const defaultMaxEventSize = buffer.kStringMaxLength
const DATA = Buffer.from('data') const DATA = Buffer.from('data')
const EVENT = Buffer.from('event') const EVENT = Buffer.from('event')
const ID = Buffer.from('id') const ID = Buffer.from('id')
const RETRY = Buffer.from('retry') const RETRY = Buffer.from('retry')
// Each data field creates a string node, and each unconsumed input chunk keeps
// a Buffer wrapper. The absolute ceiling protects the large default byte limit.
const minParserItems = 1024
const maxParserItems = 131072
const bytesPerParserItem = 128
function isASCIINumberBytes (buffer, start) { function isASCIINumberBytes (buffer, start) {
if (start >= buffer.length) { if (start >= buffer.length) {
return false return false
@@ -16957,6 +16974,12 @@ function isFieldName (line, length, field) {
return true return true
} }
function createMaxEventSizeExceededError () {
const error = new Error('EventSource message size exceeded')
error.aborted = false
return error
}
/** /**
* @typedef {object} EventSourceStreamEvent * @typedef {object} EventSourceStreamEvent
* @type {object} * @type {object}
@@ -17005,6 +17028,13 @@ class EventSourceStream extends Transform {
pos = 0 pos = 0
lineChunkIndex = 0 lineChunkIndex = 0
linePos = 0 linePos = 0
lineLength = 0
eventSize = 0
eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 }
dataLineCount = 0
maxEventSize
maxDataLines
maxBufferedChunks
event = { event = {
data: undefined, data: undefined,
@@ -17015,6 +17045,7 @@ class EventSourceStream extends Transform {
/** /**
* @param {object} options * @param {object} options
* @param {number} [options.maxEventSize]
* @param {eventSourceSettings} options.eventSourceSettings * @param {eventSourceSettings} options.eventSourceSettings
* @param {Function} [options.push] * @param {Function} [options.push]
*/ */
@@ -17026,6 +17057,12 @@ class EventSourceStream extends Transform {
super(options) super(options)
this.state = options.eventSourceSettings || {} this.state = options.eventSourceSettings || {}
this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize
const parserItemLimit = this.maxEventSize > 0
? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem)))
: 0
this.maxDataLines = parserItemLimit
this.maxBufferedChunks = parserItemLimit
if (options.push) { if (options.push) {
this.push = options.push this.push = options.push
} }
@@ -17043,6 +17080,14 @@ class EventSourceStream extends Transform {
return return
} }
// A leading line ending can immediately release the currently buffered
// line, even when it uses the final allowed chunk slot.
if (this.maxEventSize > 0 &&
this.chunks.length >= this.maxBufferedChunks &&
chunk[0] !== LF && chunk[0] !== CR) {
callback(createMaxEventSizeExceededError())
return
}
this.chunks.push(chunk) this.chunks.push(chunk)
// Strip leading byte-order-mark if we opened the stream and started // Strip leading byte-order-mark if we opened the stream and started
@@ -17121,7 +17166,12 @@ class EventSourceStream extends Transform {
// In any case, we can process the line as we reached an // In any case, we can process the line as we reached an
// end-of-line character // end-of-line character
this.parseLine(this.readLine(), this.event) try {
this.parseLine(this.readLine(), this.event)
} catch (error) {
callback(error)
return
}
this.consumeCurrentByte() this.consumeCurrentByte()
// A line was processed and this could be the end of the event. We need // A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is // to check if the next line is empty to determine if the event is
@@ -17130,15 +17180,28 @@ class EventSourceStream extends Transform {
continue continue
} }
// readLine() is only called after CR or LF. Reject an unfinished line
// before it can retain an unbounded number of decoded chunks. The seven
// extra bytes allow the longest field prefix ("event: " or "retry: ").
if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) {
callback(createMaxEventSizeExceededError())
return
}
this.advanceCursor() this.advanceCursor()
} }
callback() callback()
} }
_destroy (error, callback) {
this.chunks.length = 0
this.clearEvent()
super._destroy(error, callback)
}
/** /**
* @param {Buffer} line * @param {Buffer} line
* @param {EventStreamEvent} event * @param {EventSourceStreamEvent} event
*/ */
parseLine (line, event) { parseLine (line, event) {
// If the line is empty (a blank line) // If the line is empty (a blank line)
@@ -17172,6 +17235,13 @@ class EventSourceStream extends Transform {
} }
if (isFieldName(line, fieldLength, DATA)) { if (isFieldName(line, fieldLength, DATA)) {
if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) {
throw createMaxEventSizeExceededError()
}
const valueBytes = line.length - valueStart
const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes
this.setEventFieldSize('data', dataSize)
const value = line.toString('utf8', valueStart) const value = line.toString('utf8', valueStart)
if (event.data === undefined) { if (event.data === undefined) {
@@ -17179,11 +17249,13 @@ class EventSourceStream extends Transform {
} else { } else {
event.data += `\n${value}` event.data += `\n${value}`
} }
this.dataLineCount++
return return
} }
if (isFieldName(line, fieldLength, RETRY)) { if (isFieldName(line, fieldLength, RETRY)) {
if (isASCIINumberBytes(line, valueStart)) { if (isASCIINumberBytes(line, valueStart)) {
this.setEventFieldSize('retry', line.length - valueStart)
event.retry = line.toString('utf8', valueStart) event.retry = line.toString('utf8', valueStart)
} }
return return
@@ -17191,20 +17263,29 @@ class EventSourceStream extends Transform {
if (isFieldName(line, fieldLength, ID)) { if (isFieldName(line, fieldLength, ID)) {
if (isValidLastEventIdBytes(line, valueStart)) { if (isValidLastEventIdBytes(line, valueStart)) {
this.setEventFieldSize('id', line.length - valueStart)
event.id = line.toString('utf8', valueStart) event.id = line.toString('utf8', valueStart)
} }
return return
} }
if (isFieldName(line, fieldLength, EVENT)) { if (isFieldName(line, fieldLength, EVENT)) {
const value = line.toString('utf8', valueStart) if (valueStart < line.length) {
this.setEventFieldSize('event', line.length - valueStart)
if (value.length > 0) { event.event = line.toString('utf8', valueStart)
event.event = value
} }
} }
} }
setEventFieldSize (field, size) {
const eventSize = this.eventSize - this.eventFieldSizes[field] + size
if (this.maxEventSize > 0 && eventSize > this.maxEventSize) {
throw createMaxEventSizeExceededError()
}
this.eventSize = eventSize
this.eventFieldSizes[field] = size
}
/** /**
* @param {EventSourceStreamEvent} event * @param {EventSourceStreamEvent} event
*/ */
@@ -17235,6 +17316,12 @@ class EventSourceStream extends Transform {
this.event.event = undefined this.event.event = undefined
this.event.id = undefined this.event.id = undefined
this.event.retry = undefined this.event.retry = undefined
this.eventSize = 0
this.dataLineCount = 0
this.eventFieldSizes.data = 0
this.eventFieldSizes.event = 0
this.eventFieldSizes.id = 0
this.eventFieldSizes.retry = 0
} }
hasPendingEvent () { hasPendingEvent () {
@@ -17270,14 +17357,16 @@ class EventSourceStream extends Transform {
syncLineStartToCursor () { syncLineStartToCursor () {
this.lineChunkIndex = this.chunkIndex this.lineChunkIndex = this.chunkIndex
this.linePos = this.pos this.linePos = this.pos
this.lineLength = 0
this.dropConsumedChunks() this.dropConsumedChunks()
} }
dropConsumedChunks () { dropConsumedChunks () {
while (this.lineChunkIndex > 0) { if (this.lineChunkIndex > 0) {
this.chunks.shift() const consumed = this.lineChunkIndex
this.lineChunkIndex-- this.chunks.splice(0, consumed)
this.chunkIndex-- this.lineChunkIndex = 0
this.chunkIndex -= consumed
} }
if (this.chunkIndex === this.chunks.length) { if (this.chunkIndex === this.chunks.length) {
@@ -17402,6 +17491,7 @@ const { isNetworkError } = __nccwpck_require__(9051)
const { delay } = __nccwpck_require__(4811) const { delay } = __nccwpck_require__(4811)
const { kEnumerableProperty } = __nccwpck_require__(3440) const { kEnumerableProperty } = __nccwpck_require__(3440)
const { environmentSettingsObject } = __nccwpck_require__(3168) const { environmentSettingsObject } = __nccwpck_require__(3168)
const { getGlobalDispatcher } = __nccwpck_require__(2581)
let experimentalWarned = false let experimentalWarned = false
@@ -17683,6 +17773,7 @@ class EventSource extends EventTarget {
const eventSourceStream = new EventSourceStream({ const eventSourceStream = new EventSourceStream({
eventSourceSettings: this.#state, eventSourceSettings: this.#state,
maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize,
push: (event) => { push: (event) => {
this.dispatchEvent(createFastMessageEvent( this.dispatchEvent(createFastMessageEvent(
event.type, event.type,
@@ -17861,7 +17952,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
}, },
{ {
key: 'dispatcher', // undici only key: 'dispatcher', // undici only
converter: webidl.converters.any converter: webidl.converters.any,
defaultValue: () => getGlobalDispatcher()
} }
]) ])
@@ -22957,8 +23049,10 @@ async function httpNetworkFetch (
// 19. Run these steps in parallel: // 19. Run these steps in parallel:
// 1. Run these steps, but abort when fetchParams is canceled: // 1. Run these steps, but abort when fetchParams is canceled:
fetchParams.controller.onAborted = onAborted if (!fetchParams.controller.resume) {
fetchParams.controller.on('terminated', onAborted) fetchParams.controller.on('terminated', onAborted)
}
fetchParams.controller.resume = async () => { fetchParams.controller.resume = async () => {
// 1. While true // 1. While true
while (true) { while (true) {
@@ -23127,8 +23221,6 @@ async function httpNetworkFetch (
} }
location = headersList.get('location', true) location = headersList.get('location', true)
this.body = new Readable({ read: resume })
const decoders = [] const decoders = []
const willFollow = location && request.redirect === 'follow' && const willFollow = location && request.redirect === 'follow' &&
@@ -23146,7 +23238,7 @@ async function httpNetworkFetch (
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
const maxContentEncodings = 5 const maxContentEncodings = 5
if (codings.length > maxContentEncodings) { if (codings.length > maxContentEncodings) {
reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) this.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
return true return true
} }
@@ -23179,6 +23271,8 @@ async function httpNetworkFetch (
} }
} }
this.body = new Readable({ read: resume })
const onError = this.onError.bind(this) const onError = this.onError.bind(this)
resolve({ resolve({
@@ -23225,10 +23319,6 @@ async function httpNetworkFetch (
fetchParams.controller.off('terminated', this.abort) fetchParams.controller.off('terminated', this.abort)
} }
if (fetchParams.controller.onAborted) {
fetchParams.controller.off('terminated', fetchParams.controller.onAborted)
}
fetchParams.controller.ended = true fetchParams.controller.ended = true
this.body.push(null) this.body.push(null)
@@ -29703,6 +29793,20 @@ class ByteParser extends Writable {
if (isControlFrame(this.#info.opcode)) { if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body) this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO this.#state = parserStates.INFO
// If the automatic PONG/CLOSE reply filled the socket's write buffer,
// stop parsing and resume on drain so a PING flood cannot grow the
// outbound queue without bound.
const socket = this.ws[kResponse].socket
if (this.#loop && socket.writableNeedDrain) {
this.#loop = false
socket.once('drain', () => {
if (this.destroyed) return
this.#loop = true
this.run(callback)
})
return
}
} else { } else {
if (!this.#info.compressed) { if (!this.#info.compressed) {
if (!this.writeFragments(body)) { if (!this.writeFragments(body)) {
+134 -30
View File
@@ -7217,7 +7217,7 @@ function defaultFactory (origin, opts) {
} }
class Agent extends DispatcherBase { class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { constructor ({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) {
if (typeof factory !== 'function') { if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.') throw new InvalidArgumentError('factory must be a function.')
} }
@@ -7236,11 +7236,15 @@ class Agent extends DispatcherBase {
connect = { ...connect } connect = { ...connect }
} }
if (tls && typeof tls !== 'function') {
tls = { ...tls }
}
this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent)
? options.interceptors.Agent ? options.interceptors.Agent
: [createRedirectInterceptor({ maxRedirections })] : [createRedirectInterceptor({ maxRedirections })]
this[kOptions] = { ...util.deepClone(options), connect } this[kOptions] = { ...util.deepClone(options), connect, tls }
this[kOptions].interceptors = options.interceptors this[kOptions].interceptors = options.interceptors
? { ...options.interceptors } ? { ...options.interceptors }
: undefined : undefined
@@ -8273,8 +8277,6 @@ async function connectH1 (client, socket) {
socket[kParser] = new Parser(client, socket, llhttpInstance) socket[kParser] = new Parser(client, socket, llhttpInstance)
addListener(socket, 'error', function (err) { addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
const parser = this[kParser] const parser = this[kParser]
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
@@ -9204,8 +9206,6 @@ async function connectH2 (client, socket) {
socket[kHTTP2Session] = session socket[kHTTP2Session] = session
util.addListener(socket, 'error', function (err) { util.addListener(socket, 'error', function (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kError] = err this[kError] = err
this[kClient][kOnError](err) this[kClient][kOnError](err)
@@ -9279,8 +9279,6 @@ function resumeH2 (client) {
} }
function onHttp2SessionError (err) { function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
this[kSocket][kError] = err this[kSocket][kError] = err
this[kClient][kOnError](err) this[kClient][kOnError](err)
} }
@@ -9978,9 +9976,10 @@ class Client extends DispatcherBase {
// h2 // h2
maxConcurrentStreams, maxConcurrentStreams,
allowH2, allowH2,
webSocket webSocket,
eventSource
} = {}) { } = {}) {
super({ webSocket }) super({ webSocket, eventSource })
if (keepAlive !== undefined) { if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
@@ -10501,6 +10500,7 @@ module.exports = Client
const buffer = __nccwpck_require__(4573)
const Dispatcher = __nccwpck_require__(883) const Dispatcher = __nccwpck_require__(883)
const { const {
ClientDestroyedError, ClientDestroyedError,
@@ -10513,6 +10513,7 @@ const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed') const kOnClosed = Symbol('onClosed')
const kInterceptedDispatch = Symbol('Intercepted Dispatch') const kInterceptedDispatch = Symbol('Intercepted Dispatch')
const kWebSocketOptions = Symbol('webSocketOptions') const kWebSocketOptions = Symbol('webSocketOptions')
const kEventSourceOptions = Symbol('eventSourceOptions')
class DispatcherBase extends Dispatcher { class DispatcherBase extends Dispatcher {
constructor (opts) { constructor (opts) {
@@ -10523,6 +10524,7 @@ class DispatcherBase extends Dispatcher {
this[kClosed] = false this[kClosed] = false
this[kOnClosed] = [] this[kOnClosed] = []
this[kWebSocketOptions] = opts?.webSocket ?? {} this[kWebSocketOptions] = opts?.webSocket ?? {}
this[kEventSourceOptions] = opts?.eventSource ?? {}
} }
get webSocketOptions () { get webSocketOptions () {
@@ -10532,6 +10534,12 @@ class DispatcherBase extends Dispatcher {
} }
} }
get eventSourceOptions () {
return {
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
}
}
get destroyed () { get destroyed () {
return this[kDestroyed] return this[kDestroyed]
} }
@@ -16890,6 +16898,7 @@ module.exports = {
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const buffer = __nccwpck_require__(4573)
const { Transform } = __nccwpck_require__(7075) const { Transform } = __nccwpck_require__(7075)
const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(4811) const { isASCIINumber, isValidLastEventId } = __nccwpck_require__(4811)
@@ -16914,11 +16923,19 @@ const COLON = 0x3A
*/ */
const SPACE = 0x20 const SPACE = 0x20
const defaultMaxEventSize = buffer.kStringMaxLength
const DATA = Buffer.from('data') const DATA = Buffer.from('data')
const EVENT = Buffer.from('event') const EVENT = Buffer.from('event')
const ID = Buffer.from('id') const ID = Buffer.from('id')
const RETRY = Buffer.from('retry') const RETRY = Buffer.from('retry')
// Each data field creates a string node, and each unconsumed input chunk keeps
// a Buffer wrapper. The absolute ceiling protects the large default byte limit.
const minParserItems = 1024
const maxParserItems = 131072
const bytesPerParserItem = 128
function isASCIINumberBytes (buffer, start) { function isASCIINumberBytes (buffer, start) {
if (start >= buffer.length) { if (start >= buffer.length) {
return false return false
@@ -16957,6 +16974,12 @@ function isFieldName (line, length, field) {
return true return true
} }
function createMaxEventSizeExceededError () {
const error = new Error('EventSource message size exceeded')
error.aborted = false
return error
}
/** /**
* @typedef {object} EventSourceStreamEvent * @typedef {object} EventSourceStreamEvent
* @type {object} * @type {object}
@@ -17005,6 +17028,13 @@ class EventSourceStream extends Transform {
pos = 0 pos = 0
lineChunkIndex = 0 lineChunkIndex = 0
linePos = 0 linePos = 0
lineLength = 0
eventSize = 0
eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 }
dataLineCount = 0
maxEventSize
maxDataLines
maxBufferedChunks
event = { event = {
data: undefined, data: undefined,
@@ -17015,6 +17045,7 @@ class EventSourceStream extends Transform {
/** /**
* @param {object} options * @param {object} options
* @param {number} [options.maxEventSize]
* @param {eventSourceSettings} options.eventSourceSettings * @param {eventSourceSettings} options.eventSourceSettings
* @param {Function} [options.push] * @param {Function} [options.push]
*/ */
@@ -17026,6 +17057,12 @@ class EventSourceStream extends Transform {
super(options) super(options)
this.state = options.eventSourceSettings || {} this.state = options.eventSourceSettings || {}
this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize
const parserItemLimit = this.maxEventSize > 0
? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem)))
: 0
this.maxDataLines = parserItemLimit
this.maxBufferedChunks = parserItemLimit
if (options.push) { if (options.push) {
this.push = options.push this.push = options.push
} }
@@ -17043,6 +17080,14 @@ class EventSourceStream extends Transform {
return return
} }
// A leading line ending can immediately release the currently buffered
// line, even when it uses the final allowed chunk slot.
if (this.maxEventSize > 0 &&
this.chunks.length >= this.maxBufferedChunks &&
chunk[0] !== LF && chunk[0] !== CR) {
callback(createMaxEventSizeExceededError())
return
}
this.chunks.push(chunk) this.chunks.push(chunk)
// Strip leading byte-order-mark if we opened the stream and started // Strip leading byte-order-mark if we opened the stream and started
@@ -17121,7 +17166,12 @@ class EventSourceStream extends Transform {
// In any case, we can process the line as we reached an // In any case, we can process the line as we reached an
// end-of-line character // end-of-line character
this.parseLine(this.readLine(), this.event) try {
this.parseLine(this.readLine(), this.event)
} catch (error) {
callback(error)
return
}
this.consumeCurrentByte() this.consumeCurrentByte()
// A line was processed and this could be the end of the event. We need // A line was processed and this could be the end of the event. We need
// to check if the next line is empty to determine if the event is // to check if the next line is empty to determine if the event is
@@ -17130,15 +17180,28 @@ class EventSourceStream extends Transform {
continue continue
} }
// readLine() is only called after CR or LF. Reject an unfinished line
// before it can retain an unbounded number of decoded chunks. The seven
// extra bytes allow the longest field prefix ("event: " or "retry: ").
if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) {
callback(createMaxEventSizeExceededError())
return
}
this.advanceCursor() this.advanceCursor()
} }
callback() callback()
} }
_destroy (error, callback) {
this.chunks.length = 0
this.clearEvent()
super._destroy(error, callback)
}
/** /**
* @param {Buffer} line * @param {Buffer} line
* @param {EventStreamEvent} event * @param {EventSourceStreamEvent} event
*/ */
parseLine (line, event) { parseLine (line, event) {
// If the line is empty (a blank line) // If the line is empty (a blank line)
@@ -17172,6 +17235,13 @@ class EventSourceStream extends Transform {
} }
if (isFieldName(line, fieldLength, DATA)) { if (isFieldName(line, fieldLength, DATA)) {
if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) {
throw createMaxEventSizeExceededError()
}
const valueBytes = line.length - valueStart
const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes
this.setEventFieldSize('data', dataSize)
const value = line.toString('utf8', valueStart) const value = line.toString('utf8', valueStart)
if (event.data === undefined) { if (event.data === undefined) {
@@ -17179,11 +17249,13 @@ class EventSourceStream extends Transform {
} else { } else {
event.data += `\n${value}` event.data += `\n${value}`
} }
this.dataLineCount++
return return
} }
if (isFieldName(line, fieldLength, RETRY)) { if (isFieldName(line, fieldLength, RETRY)) {
if (isASCIINumberBytes(line, valueStart)) { if (isASCIINumberBytes(line, valueStart)) {
this.setEventFieldSize('retry', line.length - valueStart)
event.retry = line.toString('utf8', valueStart) event.retry = line.toString('utf8', valueStart)
} }
return return
@@ -17191,20 +17263,29 @@ class EventSourceStream extends Transform {
if (isFieldName(line, fieldLength, ID)) { if (isFieldName(line, fieldLength, ID)) {
if (isValidLastEventIdBytes(line, valueStart)) { if (isValidLastEventIdBytes(line, valueStart)) {
this.setEventFieldSize('id', line.length - valueStart)
event.id = line.toString('utf8', valueStart) event.id = line.toString('utf8', valueStart)
} }
return return
} }
if (isFieldName(line, fieldLength, EVENT)) { if (isFieldName(line, fieldLength, EVENT)) {
const value = line.toString('utf8', valueStart) if (valueStart < line.length) {
this.setEventFieldSize('event', line.length - valueStart)
if (value.length > 0) { event.event = line.toString('utf8', valueStart)
event.event = value
} }
} }
} }
setEventFieldSize (field, size) {
const eventSize = this.eventSize - this.eventFieldSizes[field] + size
if (this.maxEventSize > 0 && eventSize > this.maxEventSize) {
throw createMaxEventSizeExceededError()
}
this.eventSize = eventSize
this.eventFieldSizes[field] = size
}
/** /**
* @param {EventSourceStreamEvent} event * @param {EventSourceStreamEvent} event
*/ */
@@ -17235,6 +17316,12 @@ class EventSourceStream extends Transform {
this.event.event = undefined this.event.event = undefined
this.event.id = undefined this.event.id = undefined
this.event.retry = undefined this.event.retry = undefined
this.eventSize = 0
this.dataLineCount = 0
this.eventFieldSizes.data = 0
this.eventFieldSizes.event = 0
this.eventFieldSizes.id = 0
this.eventFieldSizes.retry = 0
} }
hasPendingEvent () { hasPendingEvent () {
@@ -17270,14 +17357,16 @@ class EventSourceStream extends Transform {
syncLineStartToCursor () { syncLineStartToCursor () {
this.lineChunkIndex = this.chunkIndex this.lineChunkIndex = this.chunkIndex
this.linePos = this.pos this.linePos = this.pos
this.lineLength = 0
this.dropConsumedChunks() this.dropConsumedChunks()
} }
dropConsumedChunks () { dropConsumedChunks () {
while (this.lineChunkIndex > 0) { if (this.lineChunkIndex > 0) {
this.chunks.shift() const consumed = this.lineChunkIndex
this.lineChunkIndex-- this.chunks.splice(0, consumed)
this.chunkIndex-- this.lineChunkIndex = 0
this.chunkIndex -= consumed
} }
if (this.chunkIndex === this.chunks.length) { if (this.chunkIndex === this.chunks.length) {
@@ -17402,6 +17491,7 @@ const { isNetworkError } = __nccwpck_require__(9051)
const { delay } = __nccwpck_require__(4811) const { delay } = __nccwpck_require__(4811)
const { kEnumerableProperty } = __nccwpck_require__(3440) const { kEnumerableProperty } = __nccwpck_require__(3440)
const { environmentSettingsObject } = __nccwpck_require__(3168) const { environmentSettingsObject } = __nccwpck_require__(3168)
const { getGlobalDispatcher } = __nccwpck_require__(2581)
let experimentalWarned = false let experimentalWarned = false
@@ -17683,6 +17773,7 @@ class EventSource extends EventTarget {
const eventSourceStream = new EventSourceStream({ const eventSourceStream = new EventSourceStream({
eventSourceSettings: this.#state, eventSourceSettings: this.#state,
maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize,
push: (event) => { push: (event) => {
this.dispatchEvent(createFastMessageEvent( this.dispatchEvent(createFastMessageEvent(
event.type, event.type,
@@ -17861,7 +17952,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
}, },
{ {
key: 'dispatcher', // undici only key: 'dispatcher', // undici only
converter: webidl.converters.any converter: webidl.converters.any,
defaultValue: () => getGlobalDispatcher()
} }
]) ])
@@ -22957,8 +23049,10 @@ async function httpNetworkFetch (
// 19. Run these steps in parallel: // 19. Run these steps in parallel:
// 1. Run these steps, but abort when fetchParams is canceled: // 1. Run these steps, but abort when fetchParams is canceled:
fetchParams.controller.onAborted = onAborted if (!fetchParams.controller.resume) {
fetchParams.controller.on('terminated', onAborted) fetchParams.controller.on('terminated', onAborted)
}
fetchParams.controller.resume = async () => { fetchParams.controller.resume = async () => {
// 1. While true // 1. While true
while (true) { while (true) {
@@ -23127,8 +23221,6 @@ async function httpNetworkFetch (
} }
location = headersList.get('location', true) location = headersList.get('location', true)
this.body = new Readable({ read: resume })
const decoders = [] const decoders = []
const willFollow = location && request.redirect === 'follow' && const willFollow = location && request.redirect === 'follow' &&
@@ -23146,7 +23238,7 @@ async function httpNetworkFetch (
// CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
const maxContentEncodings = 5 const maxContentEncodings = 5
if (codings.length > maxContentEncodings) { if (codings.length > maxContentEncodings) {
reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)) this.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
return true return true
} }
@@ -23179,6 +23271,8 @@ async function httpNetworkFetch (
} }
} }
this.body = new Readable({ read: resume })
const onError = this.onError.bind(this) const onError = this.onError.bind(this)
resolve({ resolve({
@@ -23225,10 +23319,6 @@ async function httpNetworkFetch (
fetchParams.controller.off('terminated', this.abort) fetchParams.controller.off('terminated', this.abort)
} }
if (fetchParams.controller.onAborted) {
fetchParams.controller.off('terminated', fetchParams.controller.onAborted)
}
fetchParams.controller.ended = true fetchParams.controller.ended = true
this.body.push(null) this.body.push(null)
@@ -29703,6 +29793,20 @@ class ByteParser extends Writable {
if (isControlFrame(this.#info.opcode)) { if (isControlFrame(this.#info.opcode)) {
this.#loop = this.parseControlFrame(body) this.#loop = this.parseControlFrame(body)
this.#state = parserStates.INFO this.#state = parserStates.INFO
// If the automatic PONG/CLOSE reply filled the socket's write buffer,
// stop parsing and resume on drain so a PING flood cannot grow the
// outbound queue without bound.
const socket = this.ws[kResponse].socket
if (this.#loop && socket.writableNeedDrain) {
this.#loop = false
socket.once('drain', () => {
if (this.destroyed) return
this.#loop = true
this.run(callback)
})
return
}
} else { } else {
if (!this.#info.compressed) { if (!this.#info.compressed) {
if (!this.writeFragments(body)) { if (!this.writeFragments(body)) {
+3 -3
View File
@@ -6375,9 +6375,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.29.0", "version": "6.29.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
"integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"