diff --git a/deps/undici/src/docs/docs/api/Client.md b/deps/undici/src/docs/docs/api/Client.md index e0d41b47805e..ca6e336f226e 100644 --- a/deps/undici/src/docs/docs/api/Client.md +++ b/deps/undici/src/docs/docs/api/Client.md @@ -29,6 +29,8 @@ Returns: `Client` * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. +* **eventSource** `EventSourceOptions` (optional) - EventSource-specific configuration options. + * **maxEventSize** `number` (optional) - The maximum total bytes of data and metadata field values retained for a pending EventSource event. Each unfinished line is separately limited to this value plus seven syntax bytes. The number of `data:` lines per event and input chunks buffered for an unfinished line are each limited to `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead. Set to `0` to disable these limits. Default: `buffer.kStringMaxLength`. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. diff --git a/deps/undici/src/docs/docs/api/DiagnosticsChannel.md b/deps/undici/src/docs/docs/api/DiagnosticsChannel.md index 099c072f6c6c..aefb1009c63a 100644 --- a/deps/undici/src/docs/docs/api/DiagnosticsChannel.md +++ b/deps/undici/src/docs/docs/api/DiagnosticsChannel.md @@ -40,7 +40,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => ## `undici:request:headers` -This message is published after the response headers have been received, i.e. the response has been completed. +This message is published after the response headers have been received. This includes a successful CONNECT or +protocol upgrade response. ```js import diagnosticsChannel from 'diagnostics_channel' @@ -57,6 +58,7 @@ diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, respo ## `undici:request:trailers` This message is published after the response body and trailers have been received, i.e. the response has been completed. +After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array. ```js import diagnosticsChannel from 'diagnostics_channel' diff --git a/deps/undici/src/lib/core/request.js b/deps/undici/src/lib/core/request.js index 8e7ecc73084b..0b2b1a8d9d7b 100644 --- a/deps/undici/src/lib/core/request.js +++ b/deps/undici/src/lib/core/request.js @@ -263,11 +263,77 @@ class Request { } } - onUpgrade (statusCode, headers, socket) { + /** + * @param {number|null} statusCode + * @param {Buffer[]|null} headers + * @param {import('node:stream').Duplex} socket + * @param {string} [statusText] + */ + onUpgrade (statusCode, headers, socket, statusText = '') { + this.onFinally() + assert(!this.aborted) assert(!this.completed) - return this[kHandler].onUpgrade(statusCode, headers, socket) + if (statusCode !== null) { + this.#publishUpgradeHeaders(statusCode, headers, statusText) + } + + const result = this[kHandler].onUpgrade(statusCode, headers, socket) + + if (!this.aborted) { + this.completed = true + if (statusCode !== null) { + this.#publishUpgradeTrailers() + } + } + + return result + } + + /** + * @param {number} statusCode + * @param {import('node:http2').IncomingHttpHeaders} headers + * @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders + * @param {string} [statusText] + */ + onUpgradeResponse (statusCode, headers, parseHeaders, statusText = '') { + assert(!this.aborted) + assert(this.completed) + + if (channels.headers.hasSubscribers) { + this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText) + } + this.#publishUpgradeTrailers() + } + + /** + * @param {Error} error + */ + onUpgradeError (error) { + assert(!this.aborted) + assert(this.completed) + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + } + + /** + * @param {number} statusCode + * @param {Buffer[]} headers + * @param {string} statusText + */ + #publishUpgradeHeaders (statusCode, headers, statusText) { + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + } + + #publishUpgradeTrailers () { + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers: [] }) + } } onComplete (trailers) { diff --git a/deps/undici/src/lib/dispatcher/agent.js b/deps/undici/src/lib/dispatcher/agent.js index 90b46fe3aeb4..416ab3edda30 100644 --- a/deps/undici/src/lib/dispatcher/agent.js +++ b/deps/undici/src/lib/dispatcher/agent.js @@ -23,7 +23,7 @@ function defaultFactory (origin, opts) { } class Agent extends DispatcherBase { - constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) { if (typeof factory !== 'function') { throw new InvalidArgumentError('factory must be a function.') } @@ -42,11 +42,15 @@ class Agent extends DispatcherBase { connect = { ...connect } } + if (tls && typeof tls !== 'function') { + tls = { ...tls } + } + this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) ? options.interceptors.Agent : [createRedirectInterceptor({ maxRedirections })] - this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions] = { ...util.deepClone(options), connect, tls } this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : undefined diff --git a/deps/undici/src/lib/dispatcher/client-h1.js b/deps/undici/src/lib/dispatcher/client-h1.js index 6b28f60c06df..7d1fdbdf6e7b 100644 --- a/deps/undici/src/lib/dispatcher/client-h1.js +++ b/deps/undici/src/lib/dispatcher/client-h1.js @@ -432,7 +432,7 @@ class Parser { } onUpgrade (head) { - const { upgrade, client, socket, headers, statusCode } = this + const { upgrade, client, socket, headers, statusCode, statusText } = this assert(upgrade) assert(client[kSocket] === socket) @@ -467,9 +467,10 @@ class Parser { client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) try { - request.onUpgrade(statusCode, headers, socket) - } catch (err) { - util.destroy(socket, err) + request.onUpgrade(statusCode, headers, socket, statusText) + } catch (error) { + util.errorRequest(client, request, error) + util.destroy(socket, error) } client[kResume]() @@ -726,8 +727,6 @@ async function connectH1 (client, socket) { socket[kParser] = new Parser(client, socket, llhttpInstance) addListener(socket, 'error', function (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - const parser = this[kParser] // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded @@ -1050,12 +1049,22 @@ function writeH1 (client, request) { const socket = client[kSocket] clearIdleSocketValidation(socket) - const abort = (err) => { - if (request.aborted || request.completed) { + /** + * @param {Error} [error] + */ + const abort = (error) => { + if (request.aborted) { + return + } + + if (request.completed) { + if (request.upgrade || request.method === 'CONNECT') { + util.destroy(socket, new InformationalError('aborted')) + } return } - util.errorRequest(client, request, err || new RequestAbortedError()) + util.errorRequest(client, request, error || new RequestAbortedError()) util.destroy(body) util.destroy(socket, new InformationalError('aborted')) diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js index 4a52effb1f3b..f417aa3d1867 100644 --- a/deps/undici/src/lib/dispatcher/client-h2.js +++ b/deps/undici/src/lib/dispatcher/client-h2.js @@ -1,6 +1,7 @@ 'use strict' const assert = require('node:assert') +const { errorMonitor } = require('node:events') const { pipeline } = require('node:stream') const util = require('../core/util.js') const { @@ -77,6 +78,15 @@ function parseH2Headers (headers) { return result } +/** + * @param {import('node:http2').IncomingHttpHeaders} headers + * @returns {Buffer[]} + */ +function parseH2ResponseHeaders (headers) { + const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers + return parseH2Headers(realHeaders) +} + async function connectH2 (client, socket) { client[kSocket] = socket @@ -126,8 +136,6 @@ async function connectH2 (client, socket) { socket[kHTTP2Session] = session util.addListener(socket, 'error', function (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kError] = err this[kClient][kOnError](err) @@ -201,8 +209,6 @@ function resumeH2 (client) { } function onHttp2SessionError (err) { - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - this[kSocket][kError] = err this[kClient][kOnError](err) } @@ -297,22 +303,32 @@ function writeH2 (client, request) { headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}` headers[HTTP2_HEADER_METHOD] = method - const abort = (err) => { - if (request.aborted || request.completed) { + /** + * @param {Error} [error] + */ + const abort = (error) => { + if (request.aborted) { return } - err = err || new RequestAbortedError() + if (request.completed) { + if (method === 'CONNECT' && stream != null) { + util.destroy(stream, error || new RequestAbortedError()) + } + return + } - util.errorRequest(client, request, err) + error = error || new RequestAbortedError() + + util.errorRequest(client, request, error) if (stream != null) { - util.destroy(stream, err) + util.destroy(stream, error) } // We do not destroy the socket as we can continue using the session // the stream get's destroyed and the session remains to create new streams - util.destroy(body, err) + util.destroy(body, error) client[kQueue][client[kRunningIdx]++] = null client[kResume]() } @@ -331,25 +347,57 @@ function writeH2 (client, request) { if (method === 'CONNECT') { session.ref() - // We are already connected, streams are pending, first request - // will create a new stream. We trigger a request to create the stream and wait until - // `ready` event is triggered // We disabled endStream to allow the user to write to the stream stream = session.request(headers, { endStream: false, signal }) + let upgradeResponseFinished = false + + /** + * @param {import('node:http2').IncomingHttpHeaders} headers + */ + const onResponse = (headers) => { + upgradeResponseFinished = true + stream.off(errorMonitor, onUpgradeError) + request.onUpgradeResponse(Number(headers[HTTP2_HEADER_STATUS]), headers, parseH2ResponseHeaders) + } - if (stream.id && !stream.pending) { - request.onUpgrade(null, null, stream) - ++session[kOpenStreams] - client[kQueue][client[kRunningIdx]++] = null - } else { - stream.once('ready', () => { + /** + * @param {Error} error + */ + const onUpgradeError = (error) => { + upgradeResponseFinished = true + stream.off('response', onResponse) + request.onUpgradeError(error) + } + + const onReady = () => { + try { request.onUpgrade(null, null, stream) - ++session[kOpenStreams] - client[kQueue][client[kRunningIdx]++] = null - }) + } catch (error) { + stream.off('response', onResponse) + abort(error) + return + } + + if (request.aborted) { + return + } + + stream.off('error', abort) + stream.once(errorMonitor, onUpgradeError) + client[kQueue][client[kRunningIdx]++] = null } + stream.once('response', onResponse) + stream.once('error', abort) + ++session[kOpenStreams] + onReady() + stream.once('close', () => { + if (!upgradeResponseFinished && request.completed) { + stream.off('response', onResponse) + stream.off(errorMonitor, onUpgradeError) + request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`)) + } session[kOpenStreams] -= 1 if (session[kOpenStreams] === 0) session.unref() }) diff --git a/deps/undici/src/lib/dispatcher/client.js b/deps/undici/src/lib/dispatcher/client.js index 18472fffd773..9efe80e3002b 100644 --- a/deps/undici/src/lib/dispatcher/client.js +++ b/deps/undici/src/lib/dispatcher/client.js @@ -107,9 +107,10 @@ class Client extends DispatcherBase { // h2 maxConcurrentStreams, allowH2, - webSocket + webSocket, + eventSource } = {}) { - super({ webSocket }) + super({ webSocket, eventSource }) if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') diff --git a/deps/undici/src/lib/dispatcher/dispatcher-base.js b/deps/undici/src/lib/dispatcher/dispatcher-base.js index 371a3ea1f6f1..e90e4b379ef9 100644 --- a/deps/undici/src/lib/dispatcher/dispatcher-base.js +++ b/deps/undici/src/lib/dispatcher/dispatcher-base.js @@ -1,5 +1,6 @@ 'use strict' +const buffer = require('node:buffer') const Dispatcher = require('./dispatcher') const { ClientDestroyedError, @@ -12,6 +13,7 @@ const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') const kInterceptedDispatch = Symbol('Intercepted Dispatch') const kWebSocketOptions = Symbol('webSocketOptions') +const kEventSourceOptions = Symbol('eventSourceOptions') class DispatcherBase extends Dispatcher { constructor (opts) { @@ -22,6 +24,7 @@ class DispatcherBase extends Dispatcher { this[kClosed] = false this[kOnClosed] = [] this[kWebSocketOptions] = opts?.webSocket ?? {} + this[kEventSourceOptions] = opts?.eventSource ?? {} } get webSocketOptions () { @@ -31,6 +34,12 @@ class DispatcherBase extends Dispatcher { } } + get eventSourceOptions () { + return { + maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength + } + } + get destroyed () { return this[kDestroyed] } diff --git a/deps/undici/src/lib/handler/retry-handler.js b/deps/undici/src/lib/handler/retry-handler.js index 2e36ab31daf1..7084c2b56520 100644 --- a/deps/undici/src/lib/handler/retry-handler.js +++ b/deps/undici/src/lib/handler/retry-handler.js @@ -205,7 +205,10 @@ class RetryHandler { this.retryCount += 1 if (statusCode >= 300) { - if (this.retryOpts.statusCodes.includes(statusCode) === false) { + // Only expose a response if no earlier attempt has reached the caller. + // Otherwise abort this attempt so the error settles the existing body + // instead of replacing it with a new response. + if (!this.headersSent && this.retryOpts.statusCodes.includes(statusCode) === false) { this.headersSent = true this.checkpointResponseEnd(headers, resume) return this.handler.onHeaders( diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index c18b346b5793..beee64378372 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,12 +1,12 @@ -> undici@6.28.1 prebuild:wasm +> undici@6.29.1 prebuild:wasm > node build/wasm.js --prebuild > docker build --platform=linux/x86_64 -t llhttp_wasm_builder -f /home/runner/work/node/node/deps/undici/src/build/Dockerfile /home/runner/work/node/node/deps/undici/src -> undici@6.28.1 build:wasm +> undici@6.29.1 build:wasm > node build/wasm.js --docker > docker run --rm -t --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js diff --git a/deps/undici/src/lib/web/eventsource/eventsource-stream.js b/deps/undici/src/lib/web/eventsource/eventsource-stream.js index af04e16322cc..9172602ad056 100644 --- a/deps/undici/src/lib/web/eventsource/eventsource-stream.js +++ b/deps/undici/src/lib/web/eventsource/eventsource-stream.js @@ -1,4 +1,5 @@ 'use strict' +const buffer = require('node:buffer') const { Transform } = require('node:stream') const { isASCIINumber, isValidLastEventId } = require('./util') @@ -23,11 +24,19 @@ const COLON = 0x3A */ const SPACE = 0x20 +const defaultMaxEventSize = buffer.kStringMaxLength + const DATA = Buffer.from('data') const EVENT = Buffer.from('event') const ID = Buffer.from('id') 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) { if (start >= buffer.length) { return false @@ -66,6 +75,12 @@ function isFieldName (line, length, field) { return true } +function createMaxEventSizeExceededError () { + const error = new Error('EventSource message size exceeded') + error.aborted = false + return error +} + /** * @typedef {object} EventSourceStreamEvent * @type {object} @@ -114,6 +129,13 @@ class EventSourceStream extends Transform { pos = 0 lineChunkIndex = 0 linePos = 0 + lineLength = 0 + eventSize = 0 + eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 } + dataLineCount = 0 + maxEventSize + maxDataLines + maxBufferedChunks event = { data: undefined, @@ -124,6 +146,7 @@ class EventSourceStream extends Transform { /** * @param {object} options + * @param {number} [options.maxEventSize] * @param {eventSourceSettings} options.eventSourceSettings * @param {Function} [options.push] */ @@ -135,6 +158,12 @@ class EventSourceStream extends Transform { super(options) 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) { this.push = options.push } @@ -152,6 +181,14 @@ class EventSourceStream extends Transform { 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) // Strip leading byte-order-mark if we opened the stream and started @@ -230,7 +267,12 @@ class EventSourceStream extends Transform { // In any case, we can process the line as we reached an // end-of-line character - this.parseLine(this.readLine(), this.event) + try { + this.parseLine(this.readLine(), this.event) + } catch (error) { + callback(error) + return + } this.consumeCurrentByte() // 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 @@ -239,15 +281,28 @@ class EventSourceStream extends Transform { 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() } callback() } + _destroy (error, callback) { + this.chunks.length = 0 + this.clearEvent() + super._destroy(error, callback) + } + /** * @param {Buffer} line - * @param {EventStreamEvent} event + * @param {EventSourceStreamEvent} event */ parseLine (line, event) { // If the line is empty (a blank line) @@ -281,6 +336,13 @@ class EventSourceStream extends Transform { } 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) if (event.data === undefined) { @@ -288,11 +350,13 @@ class EventSourceStream extends Transform { } else { event.data += `\n${value}` } + this.dataLineCount++ return } if (isFieldName(line, fieldLength, RETRY)) { if (isASCIINumberBytes(line, valueStart)) { + this.setEventFieldSize('retry', line.length - valueStart) event.retry = line.toString('utf8', valueStart) } return @@ -300,20 +364,29 @@ class EventSourceStream extends Transform { if (isFieldName(line, fieldLength, ID)) { if (isValidLastEventIdBytes(line, valueStart)) { + this.setEventFieldSize('id', line.length - valueStart) event.id = line.toString('utf8', valueStart) } return } if (isFieldName(line, fieldLength, EVENT)) { - const value = line.toString('utf8', valueStart) - - if (value.length > 0) { - event.event = value + if (valueStart < line.length) { + this.setEventFieldSize('event', line.length - valueStart) + event.event = line.toString('utf8', valueStart) } } } + 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 */ @@ -344,6 +417,12 @@ class EventSourceStream extends Transform { this.event.event = undefined this.event.id = 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 () { @@ -379,14 +458,16 @@ class EventSourceStream extends Transform { syncLineStartToCursor () { this.lineChunkIndex = this.chunkIndex this.linePos = this.pos + this.lineLength = 0 this.dropConsumedChunks() } dropConsumedChunks () { - while (this.lineChunkIndex > 0) { - this.chunks.shift() - this.lineChunkIndex-- - this.chunkIndex-- + if (this.lineChunkIndex > 0) { + const consumed = this.lineChunkIndex + this.chunks.splice(0, consumed) + this.lineChunkIndex = 0 + this.chunkIndex -= consumed } if (this.chunkIndex === this.chunks.length) { diff --git a/deps/undici/src/lib/web/eventsource/eventsource.js b/deps/undici/src/lib/web/eventsource/eventsource.js index 5a488ffce276..6092645bf2c7 100644 --- a/deps/undici/src/lib/web/eventsource/eventsource.js +++ b/deps/undici/src/lib/web/eventsource/eventsource.js @@ -11,6 +11,7 @@ const { isNetworkError } = require('../fetch/response') const { delay } = require('./util') const { kEnumerableProperty } = require('../../core/util') const { environmentSettingsObject } = require('../fetch/util') +const { getGlobalDispatcher } = require('../../global') let experimentalWarned = false @@ -292,6 +293,7 @@ class EventSource extends EventTarget { const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, + maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize, push: (event) => { this.dispatchEvent(createFastMessageEvent( event.type, @@ -470,7 +472,8 @@ webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ }, { key: 'dispatcher', // undici only - converter: webidl.converters.any + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher() } ]) diff --git a/deps/undici/src/lib/web/fetch/index.js b/deps/undici/src/lib/web/fetch/index.js index 9a685d680a1c..b3b21873dcd5 100644 --- a/deps/undici/src/lib/web/fetch/index.js +++ b/deps/undici/src/lib/web/fetch/index.js @@ -1950,8 +1950,10 @@ async function httpNetworkFetch ( // 19. Run these steps in parallel: // 1. Run these steps, but abort when fetchParams is canceled: - fetchParams.controller.onAborted = onAborted - fetchParams.controller.on('terminated', onAborted) + if (!fetchParams.controller.resume) { + fetchParams.controller.on('terminated', onAborted) + } + fetchParams.controller.resume = async () => { // 1. While true while (true) { @@ -2120,8 +2122,6 @@ async function httpNetworkFetch ( } location = headersList.get('location', true) - this.body = new Readable({ read: resume }) - const decoders = [] const willFollow = location && request.redirect === 'follow' && @@ -2139,7 +2139,7 @@ async function httpNetworkFetch ( // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206). const maxContentEncodings = 5 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 } @@ -2172,6 +2172,8 @@ async function httpNetworkFetch ( } } + this.body = new Readable({ read: resume }) + const onError = this.onError.bind(this) resolve({ @@ -2218,10 +2220,6 @@ async function httpNetworkFetch ( fetchParams.controller.off('terminated', this.abort) } - if (fetchParams.controller.onAborted) { - fetchParams.controller.off('terminated', fetchParams.controller.onAborted) - } - fetchParams.controller.ended = true this.body.push(null) diff --git a/deps/undici/src/lib/web/websocket/receiver.js b/deps/undici/src/lib/web/websocket/receiver.js index a7dea7fae1c1..b36ef9f93781 100644 --- a/deps/undici/src/lib/web/websocket/receiver.js +++ b/deps/undici/src/lib/web/websocket/receiver.js @@ -249,6 +249,20 @@ class ByteParser extends Writable { if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body) 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 { if (!this.#info.compressed) { if (!this.writeFragments(body)) { diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index ce7be625a4de..4bccc5455b89 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "6.28.1", + "version": "6.29.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/src/types/client.d.ts b/deps/undici/src/types/client.d.ts index c292b9b2651d..1ec725458128 100644 --- a/deps/undici/src/types/client.d.ts +++ b/deps/undici/src/types/client.d.ts @@ -80,6 +80,8 @@ export declare namespace Client { maxResponseSize?: number; /** WebSocket-specific options */ webSocket?: Client.WebSocketOptions; + /** EventSource-specific options */ + eventSource?: Client.EventSourceOptions; /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ autoSelectFamily?: boolean; /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ @@ -120,6 +122,18 @@ export declare namespace Client { */ maxPayloadSize?: number; } + export interface EventSourceOptions { + /** + * Maximum total bytes of data and metadata field values retained for a + * pending EventSource event. Each unfinished line is separately bounded + * to this value plus seven syntax bytes. The number of data lines per + * event and buffered input chunks are each limited to + * min(131072, max(1024, floor(maxEventSize / 128))). + * Set to 0 to disable these limits. + * @default buffer.kStringMaxLength + */ + maxEventSize?: number; + } } export default Client; diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 942c7ee11665..fbabc70fe2a5 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -574,6 +574,7 @@ var require_dispatcher = __commonJS({ var require_dispatcher_base = __commonJS({ "lib/dispatcher/dispatcher-base.js"(exports2, module2) { "use strict"; + var buffer = require("node:buffer"); var Dispatcher2 = require_dispatcher(); var { ClientDestroyedError, @@ -585,6 +586,7 @@ var require_dispatcher_base = __commonJS({ var kOnClosed = Symbol("onClosed"); var kInterceptedDispatch = Symbol("Intercepted Dispatch"); var kWebSocketOptions = Symbol("webSocketOptions"); + var kEventSourceOptions = Symbol("eventSourceOptions"); var DispatcherBase = class extends Dispatcher2 { static { __name(this, "DispatcherBase"); @@ -596,6 +598,7 @@ var require_dispatcher_base = __commonJS({ this[kClosed] = false; this[kOnClosed] = []; this[kWebSocketOptions] = opts?.webSocket ?? {}; + this[kEventSourceOptions] = opts?.eventSource ?? {}; } get webSocketOptions() { return { @@ -603,6 +606,11 @@ var require_dispatcher_base = __commonJS({ maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 }; } + get eventSourceOptions() { + return { + maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength + }; + } get destroyed() { return this[kDestroyed]; } @@ -2210,10 +2218,66 @@ var require_request = __commonJS({ return false; } } - onUpgrade(statusCode, headers, socket) { + /** + * @param {number|null} statusCode + * @param {Buffer[]|null} headers + * @param {import('node:stream').Duplex} socket + * @param {string} [statusText] + */ + onUpgrade(statusCode, headers, socket, statusText = "") { + this.onFinally(); assert(!this.aborted); assert(!this.completed); - return this[kHandler].onUpgrade(statusCode, headers, socket); + if (statusCode !== null) { + this.#publishUpgradeHeaders(statusCode, headers, statusText); + } + const result = this[kHandler].onUpgrade(statusCode, headers, socket); + if (!this.aborted) { + this.completed = true; + if (statusCode !== null) { + this.#publishUpgradeTrailers(); + } + } + return result; + } + /** + * @param {number} statusCode + * @param {import('node:http2').IncomingHttpHeaders} headers + * @param {(headers: import('node:http2').IncomingHttpHeaders) => Buffer[]} parseHeaders + * @param {string} [statusText] + */ + onUpgradeResponse(statusCode, headers, parseHeaders, statusText = "") { + assert(!this.aborted); + assert(this.completed); + if (channels.headers.hasSubscribers) { + this.#publishUpgradeHeaders(statusCode, parseHeaders(headers), statusText); + } + this.#publishUpgradeTrailers(); + } + /** + * @param {Error} error + */ + onUpgradeError(error) { + assert(!this.aborted); + assert(this.completed); + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }); + } + } + /** + * @param {number} statusCode + * @param {Buffer[]} headers + * @param {string} statusText + */ + #publishUpgradeHeaders(statusCode, headers, statusText) { + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }); + } + } + #publishUpgradeTrailers() { + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers: [] }); + } } onComplete(trailers) { this.onFinally(); @@ -6259,7 +6323,7 @@ var require_client_h1 = __commonJS({ } } onUpgrade(head) { - const { upgrade, client, socket, headers, statusCode } = this; + const { upgrade, client, socket, headers, statusCode, statusText } = this; assert(upgrade); assert(client[kSocket] === socket); assert(!socket.destroyed); @@ -6284,9 +6348,10 @@ var require_client_h1 = __commonJS({ client[kQueue][client[kRunningIdx]++] = null; client.emit("disconnect", client[kUrl], [client], new InformationalError("upgrade")); try { - request.onUpgrade(statusCode, headers, socket); - } catch (err) { - util.destroy(socket, err); + request.onUpgrade(statusCode, headers, socket, statusText); + } catch (error) { + util.errorRequest(client, request, error); + util.destroy(socket, error); } client[kResume](); } @@ -6474,7 +6539,6 @@ var require_client_h1 = __commonJS({ socket[kSocketUsed] = false; socket[kParser] = new Parser(client, socket, llhttpInstance); addListener(socket, "error", function(err) { - assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); const parser = this[kParser]; if (err.code === "ECONNRESET" && parser.statusCode && !parser.shouldKeepAlive) { const parserErr = parser.finish(); @@ -6695,11 +6759,17 @@ var require_client_h1 = __commonJS({ } const socket = client[kSocket]; clearIdleSocketValidation(socket); - const abort = /* @__PURE__ */ __name((err) => { - if (request.aborted || request.completed) { + const abort = /* @__PURE__ */ __name((error) => { + if (request.aborted) { + return; + } + if (request.completed) { + if (request.upgrade || request.method === "CONNECT") { + util.destroy(socket, new InformationalError("aborted")); + } return; } - util.errorRequest(client, request, err || new RequestAbortedError()); + util.errorRequest(client, request, error || new RequestAbortedError()); util.destroy(body); util.destroy(socket, new InformationalError("aborted")); }, "abort"); @@ -7064,6 +7134,7 @@ var require_client_h2 = __commonJS({ "lib/dispatcher/client-h2.js"(exports2, module2) { "use strict"; var assert = require("node:assert"); + var { errorMonitor } = require("node:events"); var { pipeline } = require("node:stream"); var util = require_util(); var { @@ -7125,6 +7196,11 @@ var require_client_h2 = __commonJS({ return result; } __name(parseH2Headers, "parseH2Headers"); + function parseH2ResponseHeaders(headers) { + const { [HTTP2_HEADER_STATUS]: _statusCode, ...realHeaders } = headers; + return parseH2Headers(realHeaders); + } + __name(parseH2ResponseHeaders, "parseH2ResponseHeaders"); async function connectH2(client, socket) { client[kSocket] = socket; if (!h2ExperimentalWarned) { @@ -7162,7 +7238,6 @@ var require_client_h2 = __commonJS({ client[kHTTP2Session] = session; socket[kHTTP2Session] = session; util.addListener(socket, "error", function(err) { - assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); this[kError] = err; this[kClient][kOnError](err); }); @@ -7223,7 +7298,6 @@ var require_client_h2 = __commonJS({ } __name(resumeH2, "resumeH2"); function onHttp2SessionError(err) { - assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); this[kSocket][kError] = err; this[kClient][kOnError](err); } @@ -7295,16 +7369,22 @@ var require_client_h2 = __commonJS({ const { hostname, port } = client[kUrl]; headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ""}`; headers[HTTP2_HEADER_METHOD] = method; - const abort = /* @__PURE__ */ __name((err) => { - if (request.aborted || request.completed) { + const abort = /* @__PURE__ */ __name((error) => { + if (request.aborted) { return; } - err = err || new RequestAbortedError(); - util.errorRequest(client, request, err); + if (request.completed) { + if (method === "CONNECT" && stream != null) { + util.destroy(stream, error || new RequestAbortedError()); + } + return; + } + error = error || new RequestAbortedError(); + util.errorRequest(client, request, error); if (stream != null) { - util.destroy(stream, err); + util.destroy(stream, error); } - util.destroy(body, err); + util.destroy(body, error); client[kQueue][client[kRunningIdx]++] = null; client[kResume](); }, "abort"); @@ -7319,18 +7399,42 @@ var require_client_h2 = __commonJS({ if (method === "CONNECT") { session.ref(); stream = session.request(headers, { endStream: false, signal }); - if (stream.id && !stream.pending) { - request.onUpgrade(null, null, stream); - ++session[kOpenStreams]; - client[kQueue][client[kRunningIdx]++] = null; - } else { - stream.once("ready", () => { + let upgradeResponseFinished = false; + const onResponse = /* @__PURE__ */ __name((headers2) => { + upgradeResponseFinished = true; + stream.off(errorMonitor, onUpgradeError); + request.onUpgradeResponse(Number(headers2[HTTP2_HEADER_STATUS]), headers2, parseH2ResponseHeaders); + }, "onResponse"); + const onUpgradeError = /* @__PURE__ */ __name((error) => { + upgradeResponseFinished = true; + stream.off("response", onResponse); + request.onUpgradeError(error); + }, "onUpgradeError"); + const onReady = /* @__PURE__ */ __name(() => { + try { request.onUpgrade(null, null, stream); - ++session[kOpenStreams]; - client[kQueue][client[kRunningIdx]++] = null; - }); - } + } catch (error) { + stream.off("response", onResponse); + abort(error); + return; + } + if (request.aborted) { + return; + } + stream.off("error", abort); + stream.once(errorMonitor, onUpgradeError); + client[kQueue][client[kRunningIdx]++] = null; + }, "onReady"); + stream.once("response", onResponse); + stream.once("error", abort); + ++session[kOpenStreams]; + onReady(); stream.once("close", () => { + if (!upgradeResponseFinished && request.completed) { + stream.off("response", onResponse); + stream.off(errorMonitor, onUpgradeError); + request.onUpgradeError(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`)); + } session[kOpenStreams] -= 1; if (session[kOpenStreams] === 0) session.unref(); @@ -7915,9 +8019,10 @@ var require_client = __commonJS({ // h2 maxConcurrentStreams, allowH2, - webSocket + webSocket, + eventSource } = {}) { - super({ webSocket }); + super({ webSocket, eventSource }); if (keepAlive !== void 0) { throw new InvalidArgumentError("unsupported keepAlive, use pipelining=0 instead"); } @@ -8439,7 +8544,7 @@ var require_agent = __commonJS({ static { __name(this, "Agent"); } - constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + constructor({ factory = defaultFactory, maxRedirections = 0, connect, tls, ...options } = {}) { if (typeof factory !== "function") { throw new InvalidArgumentError("factory must be a function."); } @@ -8453,8 +8558,11 @@ var require_agent = __commonJS({ if (connect && typeof connect !== "function") { connect = { ...connect }; } + if (tls && typeof tls !== "function") { + tls = { ...tls }; + } this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) ? options.interceptors.Agent : [createRedirectInterceptor({ maxRedirections })]; - this[kOptions] = { ...util.deepClone(options), connect }; + this[kOptions] = { ...util.deepClone(options), connect, tls }; this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : void 0; this[kMaxRedirections] = maxRedirections; this[kFactory] = factory; @@ -11416,8 +11524,9 @@ var require_fetch = __commonJS({ } ); response.body = { stream, source: null, length: null }; - fetchParams.controller.onAborted = onAborted; - fetchParams.controller.on("terminated", onAborted); + if (!fetchParams.controller.resume) { + fetchParams.controller.on("terminated", onAborted); + } fetchParams.controller.resume = async () => { while (true) { let bytes; @@ -11518,7 +11627,6 @@ var require_fetch = __commonJS({ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); } location = headersList.get("location", true); - this.body = new Readable({ read: resume }); const decoders = []; const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { @@ -11526,7 +11634,7 @@ var require_fetch = __commonJS({ const codings = contentEncoding ? contentEncoding.toLowerCase().split(",") : []; const maxContentEncodings = 5; 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; } for (let i = codings.length - 1; i >= 0; --i) { @@ -11556,6 +11664,7 @@ var require_fetch = __commonJS({ } } } + this.body = new Readable({ read: resume }); const onError = this.onError.bind(this); resolve({ status, @@ -11581,9 +11690,6 @@ var require_fetch = __commonJS({ if (this.abort) { fetchParams.controller.off("terminated", this.abort); } - if (fetchParams.controller.onAborted) { - fetchParams.controller.off("terminated", fetchParams.controller.onAborted); - } fetchParams.controller.ended = true; this.body.push(null); }, @@ -12706,6 +12812,17 @@ var require_receiver = __commonJS({ if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body); this.#state = parserStates.INFO; + 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 { if (!this.#info.compressed) { if (!this.writeFragments(body)) { @@ -13414,6 +13531,7 @@ var require_util4 = __commonJS({ var require_eventsource_stream = __commonJS({ "lib/web/eventsource/eventsource-stream.js"(exports2, module2) { "use strict"; + var buffer = require("node:buffer"); var { Transform } = require("node:stream"); var { isASCIINumber, isValidLastEventId } = require_util4(); var BOM = [239, 187, 191]; @@ -13421,25 +13539,29 @@ var require_eventsource_stream = __commonJS({ var CR = 13; var COLON = 58; var SPACE = 32; + var defaultMaxEventSize = buffer.kStringMaxLength; var DATA = Buffer.from("data"); var EVENT = Buffer.from("event"); var ID = Buffer.from("id"); var RETRY = Buffer.from("retry"); - function isASCIINumberBytes(buffer, start) { - if (start >= buffer.length) { + var minParserItems = 1024; + var maxParserItems = 131072; + var bytesPerParserItem = 128; + function isASCIINumberBytes(buffer2, start) { + if (start >= buffer2.length) { return false; } - for (let i = start; i < buffer.length; i++) { - if (buffer[i] < 48 || buffer[i] > 57) { + for (let i = start; i < buffer2.length; i++) { + if (buffer2[i] < 48 || buffer2[i] > 57) { return false; } } return true; } __name(isASCIINumberBytes, "isASCIINumberBytes"); - function isValidLastEventIdBytes(buffer, start) { - for (let i = start; i < buffer.length; i++) { - if (buffer[i] === 0) { + function isValidLastEventIdBytes(buffer2, start) { + for (let i = start; i < buffer2.length; i++) { + if (buffer2[i] === 0) { return false; } } @@ -13458,6 +13580,12 @@ var require_eventsource_stream = __commonJS({ return true; } __name(isFieldName, "isFieldName"); + function createMaxEventSizeExceededError() { + const error = new Error("EventSource message size exceeded"); + error.aborted = false; + return error; + } + __name(createMaxEventSizeExceededError, "createMaxEventSizeExceededError"); var EventSourceStream = class extends Transform { static { __name(this, "EventSourceStream"); @@ -13487,6 +13615,13 @@ var require_eventsource_stream = __commonJS({ pos = 0; lineChunkIndex = 0; linePos = 0; + lineLength = 0; + eventSize = 0; + eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 }; + dataLineCount = 0; + maxEventSize; + maxDataLines; + maxBufferedChunks; event = { data: void 0, event: void 0, @@ -13495,6 +13630,7 @@ var require_eventsource_stream = __commonJS({ }; /** * @param {object} options + * @param {number} [options.maxEventSize] * @param {eventSourceSettings} options.eventSourceSettings * @param {Function} [options.push] */ @@ -13502,6 +13638,10 @@ var require_eventsource_stream = __commonJS({ options.readableObjectMode = true; super(options); 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) { this.push = options.push; } @@ -13517,6 +13657,10 @@ var require_eventsource_stream = __commonJS({ callback(); return; } + if (this.maxEventSize > 0 && this.chunks.length >= this.maxBufferedChunks && chunk[0] !== LF && chunk[0] !== CR) { + callback(createMaxEventSizeExceededError()); + return; + } this.chunks.push(chunk); if (this.checkBOM) { if (this.handleBOM()) { @@ -13553,18 +13697,32 @@ var require_eventsource_stream = __commonJS({ if (byte === CR) { this.crlfCheck = true; } - this.parseLine(this.readLine(), this.event); + try { + this.parseLine(this.readLine(), this.event); + } catch (error) { + callback(error); + return; + } this.consumeCurrentByte(); this.eventEndCheck = true; continue; } + if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) { + callback(createMaxEventSizeExceededError()); + return; + } this.advanceCursor(); } callback(); } + _destroy(error, callback) { + this.chunks.length = 0; + this.clearEvent(); + super._destroy(error, callback); + } /** * @param {Buffer} line - * @param {EventStreamEvent} event + * @param {EventSourceStreamEvent} event */ parseLine(line, event) { if (line.length === 0) { @@ -13584,6 +13742,12 @@ var require_eventsource_stream = __commonJS({ } } 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 === void 0 ? 0 : 1) + valueBytes; + this.setEventFieldSize("data", dataSize); const value = line.toString("utf8", valueStart); if (event.data === void 0) { event.data = value; @@ -13591,27 +13755,38 @@ var require_eventsource_stream = __commonJS({ event.data += ` ${value}`; } + this.dataLineCount++; return; } if (isFieldName(line, fieldLength, RETRY)) { if (isASCIINumberBytes(line, valueStart)) { + this.setEventFieldSize("retry", line.length - valueStart); event.retry = line.toString("utf8", valueStart); } return; } if (isFieldName(line, fieldLength, ID)) { if (isValidLastEventIdBytes(line, valueStart)) { + this.setEventFieldSize("id", line.length - valueStart); event.id = line.toString("utf8", valueStart); } return; } if (isFieldName(line, fieldLength, EVENT)) { - const value = line.toString("utf8", valueStart); - if (value.length > 0) { - event.event = value; + if (valueStart < line.length) { + this.setEventFieldSize("event", line.length - valueStart); + event.event = line.toString("utf8", valueStart); } } } + 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 */ @@ -13638,6 +13813,12 @@ ${value}`; this.event.event = void 0; this.event.id = void 0; this.event.retry = void 0; + this.eventSize = 0; + this.dataLineCount = 0; + this.eventFieldSizes.data = 0; + this.eventFieldSizes.event = 0; + this.eventFieldSizes.id = 0; + this.eventFieldSizes.retry = 0; } hasPendingEvent() { return this.event.data !== void 0 || this.event.event !== void 0 || this.event.id !== void 0 || this.event.retry !== void 0; @@ -13662,13 +13843,15 @@ ${value}`; syncLineStartToCursor() { this.lineChunkIndex = this.chunkIndex; this.linePos = this.pos; + this.lineLength = 0; this.dropConsumedChunks(); } dropConsumedChunks() { - while (this.lineChunkIndex > 0) { - this.chunks.shift(); - this.lineChunkIndex--; - this.chunkIndex--; + if (this.lineChunkIndex > 0) { + const consumed = this.lineChunkIndex; + this.chunks.splice(0, consumed); + this.lineChunkIndex = 0; + this.chunkIndex -= consumed; } if (this.chunkIndex === this.chunks.length) { this.chunks.length = 0; @@ -13771,6 +13954,7 @@ var require_eventsource = __commonJS({ var { delay } = require_util4(); var { kEnumerableProperty } = require_util(); var { environmentSettingsObject } = require_util2(); + var { getGlobalDispatcher: getGlobalDispatcher2 } = require_global2(); var experimentalWarned = false; var defaultReconnectionTime = 3e3; var CONNECTING = 0; @@ -13915,6 +14099,7 @@ var require_eventsource = __commonJS({ this.#state.origin = response.urlList[response.urlList.length - 1].origin; const eventSourceStream = new EventSourceStream({ eventSourceSettings: this.#state, + maxEventSize: this.#dispatcher.eventSourceOptions?.maxEventSize, push: (event) => { this.dispatchEvent(createFastMessageEvent2( event.type, @@ -14050,7 +14235,8 @@ var require_eventsource = __commonJS({ { key: "dispatcher", // undici only - converter: webidl.converters.any + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher2() } ]); module2.exports = { diff --git a/src/undici_version.h b/src/undici_version.h index e2daf0e44cb2..84431a7e5615 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -2,5 +2,5 @@ // Refer to tools/dep_updaters/update-undici.sh #ifndef SRC_UNDICI_VERSION_H_ #define SRC_UNDICI_VERSION_H_ -#define UNDICI_VERSION "6.28.1" +#define UNDICI_VERSION "6.29.1" #endif // SRC_UNDICI_VERSION_H_