Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fe59f8f
fix(websocket/stream): only enqueue parsed messages in WebSocketStrea…
colinaaa Apr 3, 2026
483a4a7
fix: preserve connect option in H2CClient (#5000)
trivikr Apr 8, 2026
a399596
fix: prevent cache dedup key collision via unescaped delimiters (#501…
eddieran Apr 10, 2026
5aab0ba
fix: fix the logic for the UNDICI_NO_WASM_SIMD environment variable (…
ShenHongFei Apr 14, 2026
ee2b89e
fix(fetch): correct 'navigator' typo to 'navigate' in fetchFinale (#5…
deepview-autofix Apr 16, 2026
805493d
fix(webidl): correct signed integer bounds in ConvertToInt (#5038)
deepview-autofix Apr 16, 2026
1eaa243
fix(fetch): use || for CRLF check in multipart formdata-parser (#5049)
deepview-autofix Apr 17, 2026
39cd29a
fix(websocket): correct argument order in WebSocketStream UTF-8 failu…
deepview-autofix Apr 17, 2026
171d64d
fix(cache): evict oldest entries first in SqliteCacheStore prune (#5039)
deepview-autofix Apr 17, 2026
5a6f52f
fix(socks5): correctly expand IPv6 '::' compressed notation (#5046)
deepview-autofix Apr 17, 2026
2111c96
fix: reject malformed content-length request headers (#5060)
trivikr Apr 19, 2026
f5f3828
fix(request): reject NaN highWaterMark during option validation (#5062)
trivikr Apr 19, 2026
a79a3a4
fix(fetch): prefer filename* over filename in multipart form-data (#5…
maruthang Apr 19, 2026
731ba87
fix(cache): include query in cache key when opts.path is undefined (#…
maruthang Apr 21, 2026
a166822
fix(mock): make filterCalls AND operator actually intersect results (…
deepview-autofix Apr 21, 2026
325932c
fix(cache): skip expired sqlite vary entries during lookup (#5095)
trivikr Apr 24, 2026
a82ece9
fix: enforce maxCachedSessions in TLS session cache (#5102)
trivikr Apr 24, 2026
5c98517
fix: reuse parser WeakRef for timeout callbacks (#5125)
trivikr Apr 27, 2026
f53a334
fix: stop buffering data after SOCKS5 connect (#5118)
trivikr Apr 27, 2026
7b7b88b
fix(cache): enforce sqlite maxCount after insert (#5112)
trivikr Apr 27, 2026
7ba7a25
fix: preserve allowH2 when wrapping custom connect
mcollina Apr 28, 2026
b8c42de
test: avoid Promise.withResolvers in tls session reuse test
mcollina Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/api/api-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class RequestHandler extends AsyncResource {
throw new InvalidArgumentError('invalid callback')
}

if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) {
if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) {
throw new InvalidArgumentError('invalid highWaterMark')
}

Expand Down
6 changes: 3 additions & 3 deletions lib/cache/sqlite-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ module.exports = class SqliteCacheStore {
SELECT
id
FROM cacheInterceptorV${VERSION}
ORDER BY cachedAt DESC
ORDER BY cachedAt ASC
LIMIT ?
)
`)
Expand Down Expand Up @@ -283,7 +283,6 @@ module.exports = class SqliteCacheStore {
existingValue.id
)
} else {
this.#prune()
// New response, let's insert it
this.#insertValueQuery.run(
url,
Expand All @@ -299,6 +298,7 @@ module.exports = class SqliteCacheStore {
value.cachedAt,
value.staleAt
)
this.#prune()
}
}

Expand Down Expand Up @@ -409,7 +409,7 @@ module.exports = class SqliteCacheStore {
const now = Date.now()
for (const value of values) {
if (now >= value.deleteAt && !canBeExpired) {
return undefined
continue
}

let matches = true
Expand Down
16 changes: 16 additions & 0 deletions lib/core/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ const SessionCache = class WeakSessionCache {
return
}

if (this._sessionCache.has(sessionKey)) {
this._sessionCache.delete(sessionKey)
} else if (this._sessionCache.size >= this._maxCachedSessions) {
for (const [key, ref] of this._sessionCache) {
if (ref.deref() === undefined) {
this._sessionCache.delete(key)
return
}
}

const oldest = this._sessionCache.keys().next()
if (!oldest.done) {
this._sessionCache.delete(oldest.value)
}
}

this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
Expand Down
19 changes: 17 additions & 2 deletions lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ const { headerNameLowerCasedRecord } = require('./constants')
// Verifies that a given path is valid does not contain control chars \x00 to \x20
const invalidPathRegex = /[^\u0021-\u00ff]/

function isValidContentLengthHeaderValue (val) {
if (typeof val !== 'string' || val.length === 0) {
return false
}

for (let i = 0; i < val.length; i++) {
const charCode = val.charCodeAt(i)
if (charCode < 48 || charCode > 57) {
return false
}
}

return true
}

const kHandler = Symbol('handler')

class Request {
Expand Down Expand Up @@ -402,10 +417,10 @@ function processHeader (request, key, val) {
if (request.contentLength !== null) {
throw new InvalidArgumentError('duplicate content-length header')
}
request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) {
if (!isValidContentLengthHeaderValue(val)) {
throw new InvalidArgumentError('invalid content-length header')
}
request.contentLength = parseInt(val, 10)
} else if (request.contentType === null && headerName === 'content-type') {
request.contentType = val
request.headers.push(key, val)
Expand Down
15 changes: 10 additions & 5 deletions lib/core/socks5-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { debuglog } = require('node:util')
const { parseAddress } = require('./socks5-utils')

const debug = debuglog('undici:socks5')
const EMPTY_BUFFER = Buffer.alloc(0)

// SOCKS5 constants
const SOCKS_VERSION = 0x05
Expand Down Expand Up @@ -72,7 +73,10 @@ class Socks5Client extends EventEmitter {
this.socket = socket
this.options = options
this.state = STATES.INITIAL
this.buffer = Buffer.alloc(0)
this.buffer = EMPTY_BUFFER
this.onSocketData = this.onData.bind(this)
this.onSocketError = this.onError.bind(this)
this.onSocketClose = this.onClose.bind(this)

// Authentication settings
this.authMethods = []
Expand All @@ -82,9 +86,9 @@ class Socks5Client extends EventEmitter {
this.authMethods.push(AUTH_METHODS.NO_AUTH)

// Socket event handlers
this.socket.on('data', this.onData.bind(this))
this.socket.on('error', this.onError.bind(this))
this.socket.on('close', this.onClose.bind(this))
this.socket.on('data', this.onSocketData)
this.socket.on('error', this.onSocketError)
this.socket.on('close', this.onSocketClose)
}

/**
Expand Down Expand Up @@ -363,8 +367,9 @@ class Socks5Client extends EventEmitter {

const boundPort = this.buffer.readUInt16BE(offset)

this.buffer = this.buffer.subarray(responseLength)
this.buffer = EMPTY_BUFFER
this.state = STATES.CONNECTED
this.socket.removeListener('data', this.onSocketData)

debug('connected, bound address:', boundAddress, 'port:', boundPort)
this.emit('connected', { address: boundAddress, port: boundPort })
Expand Down
39 changes: 17 additions & 22 deletions lib/core/socks5-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,34 +46,29 @@ function parseAddress (address) {
*/
function parseIPv6 (address) {
const buffer = Buffer.alloc(16)
const parts = address.split(':')
let partIndex = 0
let bufferIndex = 0

// Handle compressed notation (::)
const doubleColonIndex = address.indexOf('::')
if (doubleColonIndex !== -1) {
// Count non-empty parts
const nonEmptyParts = parts.filter(p => p.length > 0).length
const skipParts = 8 - nonEmptyParts

for (let i = 0; i < parts.length; i++) {
if (parts[i] === '' && i === doubleColonIndex / 3) {
// Skip empty parts for ::
bufferIndex += skipParts * 2
} else if (parts[i] !== '') {
const value = parseInt(parts[i], 16)
buffer.writeUInt16BE(value, bufferIndex)
bufferIndex += 2
}
const before = address.slice(0, doubleColonIndex)
const after = address.slice(doubleColonIndex + 2)
const beforeParts = before === '' ? [] : before.split(':')
const afterParts = after === '' ? [] : after.split(':')

let bufferIndex = 0
for (const part of beforeParts) {
buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
bufferIndex += 2
}
bufferIndex = 16 - afterParts.length * 2
for (const part of afterParts) {
buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
bufferIndex += 2
}
} else {
// No compression, parse normally
for (const part of parts) {
if (part === '') continue
const value = parseInt(part, 16)
buffer.writeUInt16BE(value, partIndex * 2)
partIndex++
const parts = address.split(':')
for (let i = 0; i < parts.length; i++) {
buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2)
}
}

Expand Down
9 changes: 5 additions & 4 deletions lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ function lazyllhttp () {
let useWasmSIMD = process.arch !== 'ppc64'
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
useWasmSIMD = true
} else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
useWasmSIMD = false
} else if (process.env.UNDICI_NO_WASM_SIMD === '0') {
useWasmSIMD = true
}

if (useWasmSIMD) {
Expand Down Expand Up @@ -216,6 +216,7 @@ class Parser {
*/
this.socket = socket
this.timeout = null
this.timeoutWeakRef = new WeakRef(this)
this.timeoutValue = null
this.timeoutType = null
this.statusCode = 0
Expand Down Expand Up @@ -253,9 +254,9 @@ class Parser {

if (delay) {
if (type & USE_FAST_TIMER) {
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
this.timeout = timers.setFastTimeout(onParserTimeout, delay, this.timeoutWeakRef)
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
this.timeout = setTimeout(onParserTimeout, delay, this.timeoutWeakRef)
this.timeout?.unref()
}
}
Expand Down
8 changes: 6 additions & 2 deletions lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,13 @@ class Client extends DispatcherBase {
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
...connect
})
} else if (socketPath != null) {
} else {
const customConnect = connect
connect = (opts, callback) => customConnect({ ...opts, socketPath }, callback)
connect = (opts, callback) => customConnect({
...opts,
...(socketPath != null ? { socketPath } : null),
...(allowH2 != null ? { allowH2 } : null)
}, callback)
}

this[kUrl] = util.parseOrigin(url)
Expand Down
2 changes: 1 addition & 1 deletion lib/dispatcher/h2c-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class H2CClient extends Client {
)
}

const { connect, maxConcurrentStreams, pipelining, ...opts } =
const { maxConcurrentStreams, pipelining, ...opts } =
clientOpts ?? {}
let defaultMaxConcurrentStreams = 100
let defaultPipelining = 100
Expand Down
30 changes: 15 additions & 15 deletions lib/mock/mock-call-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
const { kMockCallHistoryAddLog } = require('./mock-symbols')
const { InvalidArgumentError } = require('../core/errors')

function handleFilterCallsWithOptions (criteria, options, handler, store) {
function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) {
switch (options.operator) {
case 'OR':
store.push(...handler(criteria))
store.push(...handler(criteria, allLogs))

return store
case 'AND':
return handler.call({ logs: store }, criteria)
return handler(criteria, store)
default:
// guard -- should never happens because buildAndValidateFilterCallsOptions is called before
throw new InvalidArgumentError('options.operator must to be a case insensitive string equal to \'OR\' or \'AND\'')
Expand All @@ -35,14 +35,14 @@ function buildAndValidateFilterCallsOptions (options = {}) {
}

function makeFilterCalls (parameterName) {
return (parameterValue) => {
return (parameterValue, logs) => {
if (typeof parameterValue === 'string' || parameterValue == null) {
return this.logs.filter((log) => {
return logs.filter((log) => {
return log[parameterName] === parameterValue
})
}
if (parameterValue instanceof RegExp) {
return this.logs.filter((log) => {
return logs.filter((log) => {
return parameterValue.test(log[parameterName])
})
}
Expand Down Expand Up @@ -175,30 +175,30 @@ class MockCallHistory {

const finalOptions = { operator: 'OR', ...buildAndValidateFilterCallsOptions(options) }

let maybeDuplicatedLogsFiltered = []
let maybeDuplicatedLogsFiltered = finalOptions.operator === 'AND' ? this.logs : []
if ('protocol' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.protocol, finalOptions, this.filterCallsByProtocol, maybeDuplicatedLogsFiltered, this.logs)
}
if ('host' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.host, finalOptions, this.filterCallsByHost, maybeDuplicatedLogsFiltered, this.logs)
}
if ('port' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.port, finalOptions, this.filterCallsByPort, maybeDuplicatedLogsFiltered, this.logs)
}
if ('origin' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.origin, finalOptions, this.filterCallsByOrigin, maybeDuplicatedLogsFiltered, this.logs)
}
if ('path' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.path, finalOptions, this.filterCallsByPath, maybeDuplicatedLogsFiltered, this.logs)
}
if ('hash' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.hash, finalOptions, this.filterCallsByHash, maybeDuplicatedLogsFiltered, this.logs)
}
if ('fullUrl' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.fullUrl, finalOptions, this.filterCallsByFullUrl, maybeDuplicatedLogsFiltered, this.logs)
}
if ('method' in criteria) {
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered)
maybeDuplicatedLogsFiltered = handleFilterCallsWithOptions(criteria.method, finalOptions, this.filterCallsByMethod, maybeDuplicatedLogsFiltered, this.logs)
}

const uniqLogsFiltered = [...new Set(maybeDuplicatedLogsFiltered)]
Expand Down
15 changes: 8 additions & 7 deletions lib/util/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function makeCacheKey (opts) {

let fullPath = opts.path || '/'

if (opts.query && !pathHasQueryOrFragment(opts.path)) {
if (opts.query && !pathHasQueryOrFragment(fullPath)) {
fullPath = serializePathWithQuery(fullPath, opts.query)
}

Expand Down Expand Up @@ -374,9 +374,11 @@ function assertCacheMethods (methods, name = 'CacheMethods') {
* @returns {string}
*/
function makeDeduplicationKey (cacheKey, excludeHeaders) {
// Create a deterministic string key from the cache key
// Include origin, method, path, and sorted headers
let key = `${cacheKey.origin}:${cacheKey.method}:${cacheKey.path}`
// Use JSON.stringify to produce a collision-resistant key.
// Previous format used `:` and `=` delimiters without escaping, which
// allowed different header sets to produce identical keys (e.g.
// {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
const headers = {}

if (cacheKey.headers) {
const sortedHeaders = Object.keys(cacheKey.headers).sort()
Expand All @@ -385,12 +387,11 @@ function makeDeduplicationKey (cacheKey, excludeHeaders) {
if (excludeHeaders?.has(header.toLowerCase())) {
continue
}
const value = cacheKey.headers[header]
key += `:${header}=${Array.isArray(value) ? value.join(',') : value}`
headers[header] = cacheKey.headers[header]
}
}

return key
return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
}

module.exports = {
Expand Down
Loading
Loading