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
19 changes: 16 additions & 3 deletions deps/undici/src/docs/docs/api/CacheStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ Parameters:
Returns: `GetResult | Promise<GetResult | undefined> | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned.
If the request isn't cached, `undefined` is returned.

The `get` method may return a `Promise` for async cache stores (e.g. Redis-backed or remote stores). The cache interceptor handles both synchronous and asynchronous return values, including in revalidation paths (304 Not Modified handling and stale-while-revalidate background revalidation).

Response properties:

* **response** `CacheValue` - The cached response data.
* **body** `Readable | undefined` - The response's body.
* **body** `Readable | Iterable<Buffer> | undefined` - The response's body. This can be an array of `Buffer` chunks (with a `.values()` method) or a `Readable` stream. Both formats are supported in all code paths, including 304 revalidation.

### Function: `createWriteStream`

Expand Down Expand Up @@ -98,8 +100,11 @@ This is an interface containing the majority of a response's data (minus the bod

### Property `vary`

`Record<string, string | string[]> | undefined` - The headers defined by the response's `Vary` header
and their respective values for later comparison
`Record<string, string | string[] | null> | undefined` - The headers defined by the response's `Vary` header
and their respective values for later comparison. Values are `null` when the
header specified in `Vary` was not present in the original request. These `null`
values are automatically filtered out during revalidation so they are not sent
as request headers.

For example, for a response like
```
Expand All @@ -116,6 +121,14 @@ This would be
}
```

If the original request did not include the `accepts` header:
```js
{
'content-encoding': 'utf8',
accepts: null
}
```

### Property `cachedAt`

`number` - Time in millis that this value was cached.
Expand Down
6 changes: 3 additions & 3 deletions deps/undici/src/docs/docs/api/WebSocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Arguments:

This example will not work in browsers or other platforms that don't allow passing an object.

```mjs
```js
import { WebSocket, ProxyAgent } from 'undici'

const proxyAgent = new ProxyAgent('my.proxy.server')
Expand All @@ -28,7 +28,7 @@ const ws = new WebSocket('wss://echo.websocket.events', {

If you do not need a custom Dispatcher, it's recommended to use the following pattern:

```mjs
```js
import { WebSocket } from 'undici'

const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
Expand All @@ -44,7 +44,7 @@ const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])

This example will not work in browsers or other platforms that don't allow passing an object.

```mjs
```js
import { Agent } from 'undici'

const agent = new Agent({ allowH2: true })
Expand Down
21 changes: 10 additions & 11 deletions deps/undici/src/lib/dispatcher/env-http-proxy-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,14 @@ class EnvHttpProxyAgent extends DispatcherBase {
if (entry.port && entry.port !== port) {
continue // Skip if ports don't match.
}
if (!/^[.*]/.test(entry.hostname)) {
// No wildcards, so don't proxy only if there is not an exact match.
if (hostname === entry.hostname) {
return false
}
} else {
// Don't proxy if the hostname ends with the no_proxy host.
if (hostname.endsWith(entry.hostname.replace(/^\*/, ''))) {
return false
}
// Don't proxy if the hostname is equal with the no_proxy host.
if (hostname === entry.hostname) {
return false
}
// Don't proxy if the hostname is the subdomain of the no_proxy host.
// Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485
if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) {
return false
}
}

Expand All @@ -123,7 +121,8 @@ class EnvHttpProxyAgent extends DispatcherBase {
}
const parsed = entry.match(/^(.+):(\d+)$/)
noProxyEntries.push({
hostname: (parsed ? parsed[1] : entry).toLowerCase(),
// strip leading dot or asterisk with dot
hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(),
port: parsed ? Number.parseInt(parsed[2], 10) : 0
})
}
Expand Down
119 changes: 77 additions & 42 deletions deps/undici/src/lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,57 +193,92 @@ class CacheHandler {
// Not modified, re-use the cached value
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
if (statusCode === 304) {
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const cachedValue = this.#store.get(this.#cacheKey)
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
}

// Re-use the cached value: statuscode, statusmessage, headers and body
value.statusCode = cachedValue.statusCode
value.statusMessage = cachedValue.statusMessage
value.etag = cachedValue.etag
value.headers = { ...cachedValue.headers, ...strippedHeaders }
const handle304 = (cachedValue) => {
if (!cachedValue) {
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
return downstreamOnHeaders()
}

downstreamOnHeaders()
// Re-use the cached value: statuscode, statusmessage, headers and body
value.statusCode = cachedValue.statusCode
value.statusMessage = cachedValue.statusMessage
value.etag = cachedValue.etag
value.headers = { ...cachedValue.headers, ...strippedHeaders }

this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
downstreamOnHeaders()

if (!this.#writeStream || !cachedValue?.body) {
return
}
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)

const bodyIterator = cachedValue.body.values()
if (!this.#writeStream || !cachedValue?.body) {
return
}

const streamCachedBody = () => {
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
if (typeof cachedValue.body.values === 'function') {
const bodyIterator = cachedValue.body.values()

const streamCachedBody = () => {
for (const chunk of bodyIterator) {
const full = this.#writeStream.write(chunk) === false
this.#handler.onResponseData?.(controller, chunk)
// when stream is full stop writing until we get a 'drain' event
if (full) {
break
}
}
}
}
}

this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('drain', () => {
this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('drain', () => {
streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})

streamCachedBody()
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
} else if (typeof cachedValue.body.on === 'function') {
// Readable stream body (e.g. from async/remote cache stores)
cachedValue.body
.on('data', (chunk) => {
this.#writeStream.write(chunk)
this.#handler.onResponseData?.(controller, chunk)
})
.on('end', () => {
this.#writeStream.end()
})
.on('error', () => {
this.#writeStream = undefined
this.#store.delete(this.#cacheKey)
})

this.#writeStream
.on('error', function () {
handler.#writeStream = undefined
handler.#store.delete(handler.#cacheKey)
})
.on('close', function () {
if (handler.#writeStream === this) {
handler.#writeStream = undefined
}
})
}
}

streamCachedBody()
/**
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
*/
const result = this.#store.get(this.#cacheKey)
if (result && typeof result.then === 'function') {
result.then(handle304)
} else {
handle304(result)
}
} else {
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
value.etag = resHeaders.etag
Expand Down
18 changes: 10 additions & 8 deletions deps/undici/src/lib/interceptor/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ function handleResult (

// Start background revalidation (fire-and-forget)
queueMicrotask(() => {
let headers = {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
Expand All @@ -302,9 +302,10 @@ function handleResult (
}

if (result.vary) {
headers = {
...headers,
...result.vary
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}

Expand Down Expand Up @@ -335,7 +336,7 @@ function handleResult (
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
}

let headers = {
const headers = {
...opts.headers,
'if-modified-since': new Date(result.cachedAt).toUTCString()
}
Expand All @@ -345,9 +346,10 @@ function handleResult (
}

if (result.vary) {
headers = {
...headers,
...result.vary
for (const key in result.vary) {
if (result.vary[key] != null) {
headers[key] = result.vary[key]
}
}
}

Expand Down
4 changes: 1 addition & 3 deletions deps/undici/src/lib/interceptor/deduplicate.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ module.exports = (opts = {}) => {
// Convert to lowercase Set for case-insensitive header exclusion from deduplication key
const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))

const safeMethodsToNotDeduplicate = util.safeHTTPMethods.filter(method => methods.includes(method) === false)

/**
* Map of pending requests for deduplication
* @type {Map<string, DeduplicationHandler>}
Expand All @@ -56,7 +54,7 @@ module.exports = (opts = {}) => {

return dispatch => {
return (opts, handler) => {
if (!opts.origin || safeMethodsToNotDeduplicate.includes(opts.method)) {
if (!opts.origin || methods.includes(opts.method) === false) {
return dispatch(opts, handler)
}

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.21.0 build:wasm
> undici@7.22.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
35 changes: 35 additions & 0 deletions deps/undici/src/lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,41 @@ async function httpNetworkFetch (
reject(error)
},

onRequestUpgrade (_controller, status, headers, socket) {
// We need to support 200 for websocket over h2 as per RFC-8441
// Absence of session means H1
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
return false
}

const headersList = new HeadersList()

for (const [name, value] of Object.entries(headers)) {
if (value == null) {
continue
}

const headerName = name.toLowerCase()

if (Array.isArray(value)) {
for (const entry of value) {
headersList.append(headerName, String(entry), true)
}
} else {
headersList.append(headerName, String(value), true)
}
}

resolve({
status,
statusText: STATUS_CODES[status],
headersList,
socket
})

return true
},

onUpgrade (status, rawHeaders, socket) {
// We need to support 200 for websocket over h2 as per RFC-8441
// Absence of session means H1
Expand Down
2 changes: 1 addition & 1 deletion deps/undici/src/lib/web/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,7 @@ function hasAuthenticationEntry (request) {
*/
function includesCredentials (url) {
// A URL includes credentials if its username or password is not the empty string.
return !!(url.username && url.password)
return !!(url.username || url.password)
}

/**
Expand Down
Loading
Loading