Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions deps/undici/src/docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Returns: `Client`
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **useH2c**: `boolean` - Default: `false`. Enforces h2c for non-https connections.
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.

> **Notes about HTTP/2**
> - It only works under TLS connections. h2c is not supported.
Expand Down
2 changes: 2 additions & 0 deletions deps/undici/src/lib/core/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ module.exports = {
kListeners: Symbol('listeners'),
kHTTPContext: Symbol('http context'),
kMaxConcurrentStreams: Symbol('max concurrent streams'),
kHTTP2InitialWindowSize: Symbol('http2 initial window size'),
kHTTP2ConnectionWindowSize: Symbol('http2 connection window size'),
kEnableConnectProtocol: Symbol('http2session connect protocol'),
kRemoteSettings: Symbol('http2session remote settings'),
kHTTP2Stream: Symbol('http2session client stream'),
Expand Down
2 changes: 2 additions & 0 deletions deps/undici/src/lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ function wrapRequestBody (body) {
// to determine whether or not it has been disturbed. This is just
// a workaround.
return new BodyAsyncIterable(body)
} else if (body && isFormDataLike(body)) {
return body
} else if (
body &&
typeof body !== 'string' &&
Expand Down
23 changes: 22 additions & 1 deletion deps/undici/src/lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const {
kOnError,
kMaxConcurrentStreams,
kHTTP2Session,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kResume,
kSize,
kHTTPContext,
Expand Down Expand Up @@ -87,12 +89,16 @@ function parseH2Headers (headers) {
function connectH2 (client, socket) {
client[kSocket] = socket

const http2InitialWindowSize = client[kHTTP2InitialWindowSize]
const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]

const session = http2.connect(client[kUrl], {
createConnection: () => socket,
peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
settings: {
// TODO(metcoder95): add support for PUSH
enablePush: false
enablePush: false,
...(http2InitialWindowSize != null ? { initialWindowSize: http2InitialWindowSize } : null)
}
})

Expand All @@ -107,6 +113,11 @@ function connectH2 (client, socket) {
// States whether or not we have received the remote settings from the server
session[kRemoteSettings] = false

// Apply connection-level flow control once connected (if supported).
if (http2ConnectionWindowSize) {
util.addListener(session, 'connect', applyConnectionWindowSize.bind(session, http2ConnectionWindowSize))
}

util.addListener(session, 'error', onHttp2SessionError)
util.addListener(session, 'frameError', onHttp2FrameError)
util.addListener(session, 'end', onHttp2SessionEnd)
Expand Down Expand Up @@ -211,6 +222,16 @@ function resumeH2 (client) {
}
}

function applyConnectionWindowSize (connectionWindowSize) {
try {
if (typeof this.setLocalWindowSize === 'function') {
this.setLocalWindowSize(connectionWindowSize)
}
} catch {
// Best-effort only.
}
}

function onHttp2RemoteSettings (settings) {
// Fallbacks are a safe bet, remote setting will always override
this[kClient][kMaxConcurrentStreams] = settings.maxConcurrentStreams ?? this[kClient][kMaxConcurrentStreams]
Expand Down
22 changes: 21 additions & 1 deletion deps/undici/src/lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const {
kOnError,
kHTTPContext,
kMaxConcurrentStreams,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
kResume
} = require('../core/symbols.js')
const connectH1 = require('./client-h1.js')
Expand Down Expand Up @@ -108,7 +110,9 @@ class Client extends DispatcherBase {
// h2
maxConcurrentStreams,
allowH2,
useH2c
useH2c,
initialWindowSize,
connectionWindowSize
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down Expand Up @@ -204,6 +208,14 @@ class Client extends DispatcherBase {
throw new InvalidArgumentError('useH2c must be a valid boolean value')
}

if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) {
throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0')
}

if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) {
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
}

super()

if (typeof connect !== 'function') {
Expand Down Expand Up @@ -239,6 +251,14 @@ class Client extends DispatcherBase {
this[kClosedResolve] = null
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
// Allows more data to be sent before requiring acknowledgment, improving throughput
// especially on high-latency networks. This matches common production HTTP/2 servers.
// - connectionWindowSize: 524288 (512KB) vs Node.js default (none set)
// Provides better flow control for the entire connection across multiple streams.
this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
this[kHTTPContext] = null

// kQueue is built up of 3 sections separated by
Expand Down
44 changes: 43 additions & 1 deletion deps/undici/src/lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ const CacheRevalidationHandler = require('../handler/cache-revalidation-handler'
const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
const { AbortError } = require('../core/errors.js')

/**
* @param {(string | RegExp)[] | undefined} origins
* @param {string} name
*/
function assertCacheOrigins (origins, name) {
if (origins === undefined) return
if (!Array.isArray(origins)) {
throw new TypeError(`expected ${name} to be an array or undefined, got ${typeof origins}`)
}
for (let i = 0; i < origins.length; i++) {
const origin = origins[i]
if (typeof origin !== 'string' && !(origin instanceof RegExp)) {
throw new TypeError(`expected ${name}[${i}] to be a string or RegExp, got ${typeof origin}`)
}
}
}

const nop = () => {}

/**
Expand Down Expand Up @@ -372,7 +389,8 @@ module.exports = (opts = {}) => {
store = new MemoryCacheStore(),
methods = ['GET'],
cacheByDefault = undefined,
type = 'shared'
type = 'shared',
origins = undefined
} = opts

if (typeof opts !== 'object' || opts === null) {
Expand All @@ -381,6 +399,7 @@ module.exports = (opts = {}) => {

assertCacheStore(store, 'opts.store')
assertCacheMethods(methods, 'opts.methods')
assertCacheOrigins(origins, 'opts.origins')

if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
Expand All @@ -406,6 +425,29 @@ module.exports = (opts = {}) => {
return dispatch(opts, handler)
}

// Check if origin is in whitelist
if (origins !== undefined) {
const requestOrigin = opts.origin.toString().toLowerCase()
let isAllowed = false

for (let i = 0; i < origins.length; i++) {
const allowed = origins[i]
if (typeof allowed === 'string') {
if (allowed.toLowerCase() === requestOrigin) {
isAllowed = true
break
}
} else if (allowed.test(requestOrigin)) {
isAllowed = true
break
}
}

if (!isAllowed) {
return dispatch(opts, handler)
}
}

opts = {
...opts,
headers: normalizeHeaders(opts)
Expand Down
2 changes: 1 addition & 1 deletion deps/undici/src/lib/llhttp/wasm_build_env.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

> undici@7.18.2 build:wasm
> undici@7.19.0 build:wasm
> node build/wasm.js --docker

> docker run --rm --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js
Expand Down
10 changes: 6 additions & 4 deletions deps/undici/src/lib/mock/mock-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const {
} = require('./mock-symbols')
const MockClient = require('./mock-client')
const MockPool = require('./mock-pool')
const { matchValue, normalizeSearchParams, buildAndValidateMockOptions } = require('./mock-utils')
const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = require('./mock-utils')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const Dispatcher = require('../dispatcher/dispatcher')
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')
Expand Down Expand Up @@ -56,9 +56,9 @@ class MockAgent extends Dispatcher {
}

get (origin) {
const originKey = this[kIgnoreTrailingSlash]
? origin.replace(/\/$/, '')
: origin
// Normalize origin to handle URL objects and case-insensitive hostnames
const normalizedOrigin = normalizeOrigin(origin)
const originKey = this[kIgnoreTrailingSlash] ? normalizedOrigin.replace(/\/$/, '') : normalizedOrigin

let dispatcher = this[kMockAgentGet](originKey)

Expand All @@ -70,6 +70,8 @@ class MockAgent extends Dispatcher {
}

dispatch (opts, handler) {
opts.origin = normalizeOrigin(opts.origin)

// Call MockAgent.get to perform additional setup before dispatching as normal
this.get(opts.origin)

Expand Down
15 changes: 14 additions & 1 deletion deps/undici/src/lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,18 @@ function checkNetConnect (netConnect, origin) {
return false
}

function normalizeOrigin (origin) {
if (typeof origin !== 'string' && !(origin instanceof URL)) {
return origin
}

if (origin instanceof URL) {
return origin.origin
}

return origin.toLowerCase()
}

function buildAndValidateMockOptions (opts) {
const { agent, ...mockOptions } = opts

Expand Down Expand Up @@ -430,5 +442,6 @@ module.exports = {
buildAndValidateMockOptions,
getHeaderByName,
buildHeadersFromArray,
normalizeSearchParams
normalizeSearchParams,
normalizeOrigin
}
4 changes: 2 additions & 2 deletions deps/undici/src/lib/web/cache/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -794,9 +794,9 @@ class Cache {
// 5.5.2
for (const response of responses) {
// 5.5.2.1
const responseObject = fromInnerResponse(response, 'immutable')
const responseObject = fromInnerResponse(cloneResponse(response), 'immutable')

responseList.push(responseObject.clone())
responseList.push(responseObject)

if (responseList.length >= maxResponses) {
break
Expand Down
81 changes: 74 additions & 7 deletions deps/undici/src/lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const {
simpleRangeHeaderValue,
buildContentRange,
createInflate,
extractMimeType
extractMimeType,
hasAuthenticationEntry,
includesCredentials,
isTraversableNavigable
} = require('./util')
const assert = require('node:assert')
const { safelyExtractBody, extractBody } = require('./body')
Expand Down Expand Up @@ -1524,13 +1527,39 @@ async function httpNetworkOrCacheFetch (

httpRequest.headersList.delete('host', true)

// 20. If includeCredentials is true, then:
// 21. If includeCredentials is true, then:
if (includeCredentials) {
// 1. If the user agent is not configured to block cookies for httpRequest
// (see section 7 of [COOKIES]), then:
// TODO: credentials

// 2. If httpRequest’s header list does not contain `Authorization`, then:
// TODO: credentials
if (!httpRequest.headersList.contains('authorization', true)) {
// 1. Let authorizationValue be null.
let authorizationValue = null

// 2. If there’s an authentication entry for httpRequest and either
// httpRequest’s use-URL-credentials flag is unset or httpRequest’s
// current URL does not include credentials, then set
// authorizationValue to authentication entry.
if (hasAuthenticationEntry(httpRequest) && (
httpRequest.useURLCredentials === undefined || !includesCredentials(requestCurrentURL(httpRequest))
)) {
// TODO
} else if (includesCredentials(requestCurrentURL(httpRequest)) && isAuthenticationFetch) {
// 3. Otherwise, if httpRequest’s current URL does include credentials
// and isAuthenticationFetch is true, set authorizationValue to
// httpRequest’s current URL, converted to an `Authorization` value
const { username, password } = requestCurrentURL(httpRequest)
authorizationValue = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
}

// 4. If authorizationValue is non-null, then append (`Authorization`,
// authorizationValue) to httpRequest’s header list.
if (authorizationValue !== null) {
httpRequest.headersList.append('Authorization', authorizationValue, false)
}
}
}

// 21. If there’s a proxy-authentication entry, use it as appropriate.
Expand Down Expand Up @@ -1612,10 +1641,48 @@ async function httpNetworkOrCacheFetch (
// 13. Set response’s request-includes-credentials to includeCredentials.
response.requestIncludesCredentials = includeCredentials

// 14. If response’s status is 401, httpRequest’s response tainting is not
// "cors", includeCredentials is true, and request’s window is an environment
// settings object, then:
// TODO
// 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
// includeCredentials is true, and request’s traversable for user prompts is
// a traversable navigable:
if (response.status === 401 && httpRequest.responseTainting !== 'cors' && includeCredentials && isTraversableNavigable(request.traversableForUserPrompts)) {
// 2. If request’s body is non-null, then:
if (request.body != null) {
// 1. If request’s body’s source is null, then return a network error.
if (request.body.source == null) {
return makeNetworkError('expected non-null body source')
}

// 2. Set request’s body to the body of the result of safely extracting
// request’s body’s source.
request.body = safelyExtractBody(request.body.source)[0]
}

// 3. If request’s use-URL-credentials flag is unset or isAuthenticationFetch is
// true, then:
if (request.useURLCredentials === undefined || isAuthenticationFetch) {
// 1. If fetchParams is canceled, then return the appropriate network error
// for fetchParams.
if (isCancelled(fetchParams)) {
return makeAppropriateNetworkError(fetchParams)
}

// 2. Let username and password be the result of prompting the end user for a
// username and password, respectively, in request’s traversable for user prompts.
// TODO

// 3. Set the username given request’s current URL and username.
// requestCurrentURL(request).username = TODO

// 4. Set the password given request’s current URL and password.
// requestCurrentURL(request).password = TODO
}

// 4. Set response to the result of running HTTP-network-or-cache fetch given
// fetchParams and true.
fetchParams.controller.connection.destroy()

response = await httpNetworkOrCacheFetch(fetchParams, true)
}

// 15. If response’s status is 407, then:
if (response.status === 407) {
Expand Down
2 changes: 2 additions & 0 deletions deps/undici/src/lib/web/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,8 @@ function makeRequest (init) {
preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false,
done: init.done ?? false,
timingAllowFailed: init.timingAllowFailed ?? false,
useURLCredentials: init.useURLCredentials ?? undefined,
traversableForUserPrompts: init.traversableForUserPrompts ?? 'client',
urlList: init.urlList,
url: init.urlList[0],
headersList: init.headersList
Expand Down
Loading
Loading