undici 7.20.0 → 7.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/CacheStore.md +16 -3
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/H2CClient.md +1 -0
- package/docs/docs/api/WebSocket.md +3 -3
- package/index-fetch.js +9 -1
- package/index.js +9 -1
- package/lib/core/symbols.js +1 -0
- package/lib/dispatcher/agent.js +3 -1
- package/lib/dispatcher/client-h2.js +40 -3
- package/lib/dispatcher/client.js +11 -3
- package/lib/dispatcher/env-http-proxy-agent.js +10 -11
- package/lib/dispatcher/pool-base.js +10 -4
- package/lib/handler/cache-handler.js +77 -42
- package/lib/interceptor/cache.js +10 -8
- package/lib/interceptor/deduplicate.js +1 -3
- package/lib/web/fetch/body.js +4 -8
- package/lib/web/fetch/index.js +40 -11
- package/lib/web/fetch/response.js +2 -1
- package/lib/web/fetch/util.js +1 -1
- package/package.json +19 -19
- package/types/client.d.ts +5 -0
- package/types/websocket.d.ts +2 -0
|
@@ -66,10 +66,12 @@ Parameters:
|
|
|
66
66
|
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.
|
|
67
67
|
If the request isn't cached, `undefined` is returned.
|
|
68
68
|
|
|
69
|
+
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).
|
|
70
|
+
|
|
69
71
|
Response properties:
|
|
70
72
|
|
|
71
73
|
* **response** `CacheValue` - The cached response data.
|
|
72
|
-
* **body** `Readable | undefined` - The response's body.
|
|
74
|
+
* **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.
|
|
73
75
|
|
|
74
76
|
### Function: `createWriteStream`
|
|
75
77
|
|
|
@@ -98,8 +100,11 @@ This is an interface containing the majority of a response's data (minus the bod
|
|
|
98
100
|
|
|
99
101
|
### Property `vary`
|
|
100
102
|
|
|
101
|
-
`Record<string, string | string[]> | undefined` - The headers defined by the response's `Vary` header
|
|
102
|
-
and their respective values for later comparison
|
|
103
|
+
`Record<string, string | string[] | null> | undefined` - The headers defined by the response's `Vary` header
|
|
104
|
+
and their respective values for later comparison. Values are `null` when the
|
|
105
|
+
header specified in `Vary` was not present in the original request. These `null`
|
|
106
|
+
values are automatically filtered out during revalidation so they are not sent
|
|
107
|
+
as request headers.
|
|
103
108
|
|
|
104
109
|
For example, for a response like
|
|
105
110
|
```
|
|
@@ -116,6 +121,14 @@ This would be
|
|
|
116
121
|
}
|
|
117
122
|
```
|
|
118
123
|
|
|
124
|
+
If the original request did not include the `accepts` header:
|
|
125
|
+
```js
|
|
126
|
+
{
|
|
127
|
+
'content-encoding': 'utf8',
|
|
128
|
+
accepts: null
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
119
132
|
### Property `cachedAt`
|
|
120
133
|
|
|
121
134
|
`number` - Time in millis that this value was cached.
|
package/docs/docs/api/Client.md
CHANGED
|
@@ -34,6 +34,7 @@ Returns: `Client`
|
|
|
34
34
|
* **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.
|
|
35
35
|
* **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.
|
|
36
36
|
* **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.
|
|
37
|
+
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.
|
|
37
38
|
|
|
38
39
|
> **Notes about HTTP/2**
|
|
39
40
|
> - It only works under TLS connections. h2c is not supported.
|
|
@@ -48,6 +48,7 @@ Returns: `H2CClient`
|
|
|
48
48
|
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
49
49
|
- **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.
|
|
50
50
|
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time.
|
|
51
|
+
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
|
|
51
52
|
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
|
|
52
53
|
- **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. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
53
54
|
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
|
|
@@ -15,7 +15,7 @@ Arguments:
|
|
|
15
15
|
|
|
16
16
|
This example will not work in browsers or other platforms that don't allow passing an object.
|
|
17
17
|
|
|
18
|
-
```
|
|
18
|
+
```js
|
|
19
19
|
import { WebSocket, ProxyAgent } from 'undici'
|
|
20
20
|
|
|
21
21
|
const proxyAgent = new ProxyAgent('my.proxy.server')
|
|
@@ -28,7 +28,7 @@ const ws = new WebSocket('wss://echo.websocket.events', {
|
|
|
28
28
|
|
|
29
29
|
If you do not need a custom Dispatcher, it's recommended to use the following pattern:
|
|
30
30
|
|
|
31
|
-
```
|
|
31
|
+
```js
|
|
32
32
|
import { WebSocket } from 'undici'
|
|
33
33
|
|
|
34
34
|
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
|
|
@@ -44,7 +44,7 @@ const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
|
|
|
44
44
|
|
|
45
45
|
This example will not work in browsers or other platforms that don't allow passing an object.
|
|
46
46
|
|
|
47
|
-
```
|
|
47
|
+
```js
|
|
48
48
|
import { Agent } from 'undici'
|
|
49
49
|
|
|
50
50
|
const agent = new Agent({ allowH2: true })
|
package/index-fetch.js
CHANGED
|
@@ -4,6 +4,10 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
|
|
4
4
|
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
|
|
5
5
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
6
6
|
|
|
7
|
+
// Capture __filename at module load time for stack trace augmentation.
|
|
8
|
+
// This may be undefined when bundled in environments like Node.js internals.
|
|
9
|
+
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
|
|
10
|
+
|
|
7
11
|
function appendFetchStackTrace (err, filename) {
|
|
8
12
|
if (!err || typeof err !== 'object') {
|
|
9
13
|
return
|
|
@@ -30,7 +34,11 @@ function appendFetchStackTrace (err, filename) {
|
|
|
30
34
|
|
|
31
35
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
32
36
|
return fetchImpl(init, options).catch(err => {
|
|
33
|
-
|
|
37
|
+
if (currentFilename) {
|
|
38
|
+
appendFetchStackTrace(err, currentFilename)
|
|
39
|
+
} else if (err && typeof err === 'object') {
|
|
40
|
+
Error.captureStackTrace(err, module.exports.fetch)
|
|
41
|
+
}
|
|
34
42
|
throw err
|
|
35
43
|
})
|
|
36
44
|
}
|
package/index.js
CHANGED
|
@@ -121,6 +121,10 @@ module.exports.getGlobalDispatcher = getGlobalDispatcher
|
|
|
121
121
|
|
|
122
122
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
123
123
|
|
|
124
|
+
// Capture __filename at module load time for stack trace augmentation.
|
|
125
|
+
// This may be undefined when bundled in environments like Node.js internals.
|
|
126
|
+
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
|
|
127
|
+
|
|
124
128
|
function appendFetchStackTrace (err, filename) {
|
|
125
129
|
if (!err || typeof err !== 'object') {
|
|
126
130
|
return
|
|
@@ -147,7 +151,11 @@ function appendFetchStackTrace (err, filename) {
|
|
|
147
151
|
|
|
148
152
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
149
153
|
return fetchImpl(init, options).catch(err => {
|
|
150
|
-
|
|
154
|
+
if (currentFilename) {
|
|
155
|
+
appendFetchStackTrace(err, currentFilename)
|
|
156
|
+
} else if (err && typeof err === 'object') {
|
|
157
|
+
Error.captureStackTrace(err, module.exports.fetch)
|
|
158
|
+
}
|
|
151
159
|
throw err
|
|
152
160
|
})
|
|
153
161
|
}
|
package/lib/core/symbols.js
CHANGED
|
@@ -67,6 +67,7 @@ module.exports = {
|
|
|
67
67
|
kEnableConnectProtocol: Symbol('http2session connect protocol'),
|
|
68
68
|
kRemoteSettings: Symbol('http2session remote settings'),
|
|
69
69
|
kHTTP2Stream: Symbol('http2session client stream'),
|
|
70
|
+
kPingInterval: Symbol('ping interval'),
|
|
70
71
|
kNoProxyAgent: Symbol('no proxy agent'),
|
|
71
72
|
kHttpProxyAgent: Symbol('http proxy agent'),
|
|
72
73
|
kHttpsProxyAgent: Symbol('https proxy agent')
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -92,7 +92,9 @@ class Agent extends DispatcherBase {
|
|
|
92
92
|
if (connected) result.count -= 1
|
|
93
93
|
if (result.count <= 0) {
|
|
94
94
|
this[kClients].delete(key)
|
|
95
|
-
result.dispatcher.
|
|
95
|
+
if (!result.dispatcher.destroyed) {
|
|
96
|
+
result.dispatcher.close()
|
|
97
|
+
}
|
|
96
98
|
}
|
|
97
99
|
this[kOrigins].delete(key)
|
|
98
100
|
}
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
kStrictContentLength,
|
|
25
25
|
kOnError,
|
|
26
26
|
kMaxConcurrentStreams,
|
|
27
|
+
kPingInterval,
|
|
27
28
|
kHTTP2Session,
|
|
28
29
|
kHTTP2InitialWindowSize,
|
|
29
30
|
kHTTP2ConnectionWindowSize,
|
|
@@ -34,7 +35,8 @@ const {
|
|
|
34
35
|
kBodyTimeout,
|
|
35
36
|
kEnableConnectProtocol,
|
|
36
37
|
kRemoteSettings,
|
|
37
|
-
kHTTP2Stream
|
|
38
|
+
kHTTP2Stream,
|
|
39
|
+
kHTTP2SessionState
|
|
38
40
|
} = require('../core/symbols.js')
|
|
39
41
|
const { channels } = require('../core/diagnostics.js')
|
|
40
42
|
|
|
@@ -102,10 +104,15 @@ function connectH2 (client, socket) {
|
|
|
102
104
|
}
|
|
103
105
|
})
|
|
104
106
|
|
|
107
|
+
client[kSocket] = socket
|
|
105
108
|
session[kOpenStreams] = 0
|
|
106
109
|
session[kClient] = client
|
|
107
110
|
session[kSocket] = socket
|
|
108
|
-
session[
|
|
111
|
+
session[kHTTP2SessionState] = {
|
|
112
|
+
ping: {
|
|
113
|
+
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
109
116
|
// We set it to true by default in a best-effort; however once connected to an H2 server
|
|
110
117
|
// we will check if extended CONNECT protocol is supported or not
|
|
111
118
|
// and set this value accordingly.
|
|
@@ -253,6 +260,31 @@ function onHttp2RemoteSettings (settings) {
|
|
|
253
260
|
this[kClient][kResume]()
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
function onHttp2SendPing (session) {
|
|
264
|
+
const state = session[kHTTP2SessionState]
|
|
265
|
+
if ((session.closed || session.destroyed) && state.ping.interval != null) {
|
|
266
|
+
clearInterval(state.ping.interval)
|
|
267
|
+
state.ping.interval = null
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If no ping sent, do nothing
|
|
272
|
+
session.ping(onPing.bind(session))
|
|
273
|
+
|
|
274
|
+
function onPing (err, duration) {
|
|
275
|
+
const client = this[kClient]
|
|
276
|
+
const socket = this[kClient]
|
|
277
|
+
|
|
278
|
+
if (err != null) {
|
|
279
|
+
const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
|
|
280
|
+
socket[kError] = error
|
|
281
|
+
client[kOnError](error)
|
|
282
|
+
} else {
|
|
283
|
+
client.emit('ping', duration)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
256
288
|
function onHttp2SessionError (err) {
|
|
257
289
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
258
290
|
|
|
@@ -316,7 +348,7 @@ function onHttp2SessionGoAway (errorCode) {
|
|
|
316
348
|
}
|
|
317
349
|
|
|
318
350
|
function onHttp2SessionClose () {
|
|
319
|
-
const { [kClient]: client } = this
|
|
351
|
+
const { [kClient]: client, [kHTTP2SessionState]: state } = this
|
|
320
352
|
const { [kSocket]: socket } = client
|
|
321
353
|
|
|
322
354
|
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
|
|
@@ -324,6 +356,11 @@ function onHttp2SessionClose () {
|
|
|
324
356
|
client[kSocket] = null
|
|
325
357
|
client[kHTTPContext] = null
|
|
326
358
|
|
|
359
|
+
if (state.ping.interval != null) {
|
|
360
|
+
clearInterval(state.ping.interval)
|
|
361
|
+
state.ping.interval = null
|
|
362
|
+
}
|
|
363
|
+
|
|
327
364
|
if (client.destroyed) {
|
|
328
365
|
assert(client[kPending] === 0)
|
|
329
366
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -54,7 +54,8 @@ const {
|
|
|
54
54
|
kMaxConcurrentStreams,
|
|
55
55
|
kHTTP2InitialWindowSize,
|
|
56
56
|
kHTTP2ConnectionWindowSize,
|
|
57
|
-
kResume
|
|
57
|
+
kResume,
|
|
58
|
+
kPingInterval
|
|
58
59
|
} = require('../core/symbols.js')
|
|
59
60
|
const connectH1 = require('./client-h1.js')
|
|
60
61
|
const connectH2 = require('./client-h2.js')
|
|
@@ -112,7 +113,8 @@ class Client extends DispatcherBase {
|
|
|
112
113
|
allowH2,
|
|
113
114
|
useH2c,
|
|
114
115
|
initialWindowSize,
|
|
115
|
-
connectionWindowSize
|
|
116
|
+
connectionWindowSize,
|
|
117
|
+
pingInterval
|
|
116
118
|
} = {}) {
|
|
117
119
|
if (keepAlive !== undefined) {
|
|
118
120
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -216,6 +218,10 @@ class Client extends DispatcherBase {
|
|
|
216
218
|
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
|
|
222
|
+
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
super()
|
|
220
226
|
|
|
221
227
|
if (typeof connect !== 'function') {
|
|
@@ -250,6 +256,8 @@ class Client extends DispatcherBase {
|
|
|
250
256
|
this[kMaxRequests] = maxRequestsPerClient
|
|
251
257
|
this[kClosedResolve] = null
|
|
252
258
|
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
|
|
259
|
+
this[kHTTPContext] = null
|
|
260
|
+
// h2
|
|
253
261
|
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
|
|
254
262
|
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
|
|
255
263
|
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
|
|
@@ -259,7 +267,7 @@ class Client extends DispatcherBase {
|
|
|
259
267
|
// Provides better flow control for the entire connection across multiple streams.
|
|
260
268
|
this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
|
|
261
269
|
this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
|
|
262
|
-
this[
|
|
270
|
+
this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute
|
|
263
271
|
|
|
264
272
|
// kQueue is built up of 3 sections separated by
|
|
265
273
|
// the kRunningIdx and kPendingIdx indices.
|
|
@@ -95,16 +95,14 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
95
95
|
if (entry.port && entry.port !== port) {
|
|
96
96
|
continue // Skip if ports don't match.
|
|
97
97
|
}
|
|
98
|
-
if
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return false
|
|
107
|
-
}
|
|
98
|
+
// Don't proxy if the hostname is equal with the no_proxy host.
|
|
99
|
+
if (hostname === entry.hostname) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
// Don't proxy if the hostname is the subdomain of the no_proxy host.
|
|
103
|
+
// Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485
|
|
104
|
+
if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) {
|
|
105
|
+
return false
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
|
|
@@ -123,7 +121,8 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
123
121
|
}
|
|
124
122
|
const parsed = entry.match(/^(.+):(\d+)$/)
|
|
125
123
|
noProxyEntries.push({
|
|
126
|
-
|
|
124
|
+
// strip leading dot or asterisk with dot
|
|
125
|
+
hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(),
|
|
127
126
|
port: parsed ? Number.parseInt(parsed[2], 10) : 0
|
|
128
127
|
})
|
|
129
128
|
}
|
|
@@ -48,9 +48,12 @@ class PoolBase extends DispatcherBase {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (this[kClosedResolve] && queue.isEmpty()) {
|
|
51
|
-
const closeAll =
|
|
51
|
+
const closeAll = []
|
|
52
52
|
for (let i = 0; i < this[kClients].length; i++) {
|
|
53
|
-
|
|
53
|
+
const client = this[kClients][i]
|
|
54
|
+
if (!client.destroyed) {
|
|
55
|
+
closeAll.push(client.close())
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
return Promise.all(closeAll)
|
|
56
59
|
.then(this[kClosedResolve])
|
|
@@ -119,9 +122,12 @@ class PoolBase extends DispatcherBase {
|
|
|
119
122
|
|
|
120
123
|
[kClose] () {
|
|
121
124
|
if (this[kQueue].isEmpty()) {
|
|
122
|
-
const closeAll =
|
|
125
|
+
const closeAll = []
|
|
123
126
|
for (let i = 0; i < this[kClients].length; i++) {
|
|
124
|
-
|
|
127
|
+
const client = this[kClients][i]
|
|
128
|
+
if (!client.destroyed) {
|
|
129
|
+
closeAll.push(client.close())
|
|
130
|
+
}
|
|
125
131
|
}
|
|
126
132
|
return Promise.all(closeAll)
|
|
127
133
|
} else {
|
|
@@ -193,57 +193,92 @@ class CacheHandler {
|
|
|
193
193
|
// Not modified, re-use the cached value
|
|
194
194
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-304-not-modified
|
|
195
195
|
if (statusCode === 304) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
|
|
202
|
-
return downstreamOnHeaders()
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Re-use the cached value: statuscode, statusmessage, headers and body
|
|
206
|
-
value.statusCode = cachedValue.statusCode
|
|
207
|
-
value.statusMessage = cachedValue.statusMessage
|
|
208
|
-
value.etag = cachedValue.etag
|
|
209
|
-
value.headers = { ...cachedValue.headers, ...strippedHeaders }
|
|
196
|
+
const handle304 = (cachedValue) => {
|
|
197
|
+
if (!cachedValue) {
|
|
198
|
+
// Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
|
|
199
|
+
return downstreamOnHeaders()
|
|
200
|
+
}
|
|
210
201
|
|
|
211
|
-
|
|
202
|
+
// Re-use the cached value: statuscode, statusmessage, headers and body
|
|
203
|
+
value.statusCode = cachedValue.statusCode
|
|
204
|
+
value.statusMessage = cachedValue.statusMessage
|
|
205
|
+
value.etag = cachedValue.etag
|
|
206
|
+
value.headers = { ...cachedValue.headers, ...strippedHeaders }
|
|
212
207
|
|
|
213
|
-
|
|
208
|
+
downstreamOnHeaders()
|
|
214
209
|
|
|
215
|
-
|
|
216
|
-
return
|
|
217
|
-
}
|
|
210
|
+
this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
|
|
218
211
|
|
|
219
|
-
|
|
212
|
+
if (!this.#writeStream || !cachedValue?.body) {
|
|
213
|
+
return
|
|
214
|
+
}
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
if (typeof cachedValue.body.values === 'function') {
|
|
217
|
+
const bodyIterator = cachedValue.body.values()
|
|
218
|
+
|
|
219
|
+
const streamCachedBody = () => {
|
|
220
|
+
for (const chunk of bodyIterator) {
|
|
221
|
+
const full = this.#writeStream.write(chunk) === false
|
|
222
|
+
this.#handler.onResponseData?.(controller, chunk)
|
|
223
|
+
// when stream is full stop writing until we get a 'drain' event
|
|
224
|
+
if (full) {
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
228
|
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
this.#writeStream
|
|
231
|
+
.on('error', function () {
|
|
232
|
+
handler.#writeStream = undefined
|
|
233
|
+
handler.#store.delete(handler.#cacheKey)
|
|
234
|
+
})
|
|
235
|
+
.on('drain', () => {
|
|
236
|
+
streamCachedBody()
|
|
237
|
+
})
|
|
238
|
+
.on('close', function () {
|
|
239
|
+
if (handler.#writeStream === this) {
|
|
240
|
+
handler.#writeStream = undefined
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
238
244
|
streamCachedBody()
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
} else if (typeof cachedValue.body.on === 'function') {
|
|
246
|
+
// Readable stream body (e.g. from async/remote cache stores)
|
|
247
|
+
cachedValue.body
|
|
248
|
+
.on('data', (chunk) => {
|
|
249
|
+
this.#writeStream.write(chunk)
|
|
250
|
+
this.#handler.onResponseData?.(controller, chunk)
|
|
251
|
+
})
|
|
252
|
+
.on('end', () => {
|
|
253
|
+
this.#writeStream.end()
|
|
254
|
+
})
|
|
255
|
+
.on('error', () => {
|
|
256
|
+
this.#writeStream = undefined
|
|
257
|
+
this.#store.delete(this.#cacheKey)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
this.#writeStream
|
|
261
|
+
.on('error', function () {
|
|
262
|
+
handler.#writeStream = undefined
|
|
263
|
+
handler.#store.delete(handler.#cacheKey)
|
|
264
|
+
})
|
|
265
|
+
.on('close', function () {
|
|
266
|
+
if (handler.#writeStream === this) {
|
|
267
|
+
handler.#writeStream = undefined
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
245
272
|
|
|
246
|
-
|
|
273
|
+
/**
|
|
274
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
|
|
275
|
+
*/
|
|
276
|
+
const result = this.#store.get(this.#cacheKey)
|
|
277
|
+
if (result && typeof result.then === 'function') {
|
|
278
|
+
result.then(handle304)
|
|
279
|
+
} else {
|
|
280
|
+
handle304(result)
|
|
281
|
+
}
|
|
247
282
|
} else {
|
|
248
283
|
if (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) {
|
|
249
284
|
value.etag = resHeaders.etag
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -292,7 +292,7 @@ function handleResult (
|
|
|
292
292
|
|
|
293
293
|
// Start background revalidation (fire-and-forget)
|
|
294
294
|
queueMicrotask(() => {
|
|
295
|
-
|
|
295
|
+
const headers = {
|
|
296
296
|
...opts.headers,
|
|
297
297
|
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
298
298
|
}
|
|
@@ -302,9 +302,10 @@ function handleResult (
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (result.vary) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
for (const key in result.vary) {
|
|
306
|
+
if (result.vary[key] != null) {
|
|
307
|
+
headers[key] = result.vary[key]
|
|
308
|
+
}
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
|
|
@@ -335,7 +336,7 @@ function handleResult (
|
|
|
335
336
|
withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
|
|
339
|
+
const headers = {
|
|
339
340
|
...opts.headers,
|
|
340
341
|
'if-modified-since': new Date(result.cachedAt).toUTCString()
|
|
341
342
|
}
|
|
@@ -345,9 +346,10 @@ function handleResult (
|
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
if (result.vary) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
for (const key in result.vary) {
|
|
350
|
+
if (result.vary[key] != null) {
|
|
351
|
+
headers[key] = result.vary[key]
|
|
352
|
+
}
|
|
351
353
|
}
|
|
352
354
|
}
|
|
353
355
|
|
|
@@ -46,8 +46,6 @@ module.exports = (opts = {}) => {
|
|
|
46
46
|
// Convert to lowercase Set for case-insensitive header exclusion from deduplication key
|
|
47
47
|
const excludeHeaderNamesSet = new Set(excludeHeaderNames.map(name => name.toLowerCase()))
|
|
48
48
|
|
|
49
|
-
const safeMethodsToNotDeduplicate = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
|
|
50
|
-
|
|
51
49
|
/**
|
|
52
50
|
* Map of pending requests for deduplication
|
|
53
51
|
* @type {Map<string, DeduplicationHandler>}
|
|
@@ -56,7 +54,7 @@ module.exports = (opts = {}) => {
|
|
|
56
54
|
|
|
57
55
|
return dispatch => {
|
|
58
56
|
return (opts, handler) => {
|
|
59
|
-
if (!opts.origin ||
|
|
57
|
+
if (!opts.origin || methods.includes(opts.method) === false) {
|
|
60
58
|
return dispatch(opts, handler)
|
|
61
59
|
}
|
|
62
60
|
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -422,18 +422,14 @@ function consumeBody (object, convertBytesToJSValue, instance, getInternalState)
|
|
|
422
422
|
return Promise.reject(e)
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
|
|
425
|
+
object = getInternalState(object)
|
|
426
426
|
|
|
427
427
|
// 1. If object is unusable, then return a promise rejected
|
|
428
428
|
// with a TypeError.
|
|
429
|
-
if (bodyUnusable(
|
|
429
|
+
if (bodyUnusable(object)) {
|
|
430
430
|
return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
if (state.aborted) {
|
|
434
|
-
return Promise.reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
435
|
-
}
|
|
436
|
-
|
|
437
433
|
// 2. Let promise be a new promise.
|
|
438
434
|
const promise = createDeferredPromise()
|
|
439
435
|
|
|
@@ -454,14 +450,14 @@ function consumeBody (object, convertBytesToJSValue, instance, getInternalState)
|
|
|
454
450
|
|
|
455
451
|
// 5. If object’s body is null, then run successSteps with an
|
|
456
452
|
// empty byte sequence.
|
|
457
|
-
if (
|
|
453
|
+
if (object.body == null) {
|
|
458
454
|
successSteps(Buffer.allocUnsafe(0))
|
|
459
455
|
return promise.promise
|
|
460
456
|
}
|
|
461
457
|
|
|
462
458
|
// 6. Otherwise, fully read object’s body given successSteps,
|
|
463
459
|
// errorSteps, and object’s relevant global object.
|
|
464
|
-
fullyReadBody(
|
|
460
|
+
fullyReadBody(object.body, successSteps, errorSteps)
|
|
465
461
|
|
|
466
462
|
// 7. Return promise.
|
|
467
463
|
return promise.promise
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -157,7 +157,7 @@ function fetch (input, init = undefined) {
|
|
|
157
157
|
if (requestObject.signal.aborted) {
|
|
158
158
|
// 1. Abort the fetch() call with p, request, null, and
|
|
159
159
|
// requestObject’s signal’s abort reason.
|
|
160
|
-
abortFetch(p, request, null, requestObject.signal.reason)
|
|
160
|
+
abortFetch(p, request, null, requestObject.signal.reason, null)
|
|
161
161
|
|
|
162
162
|
// 2. Return p.
|
|
163
163
|
return p.promise
|
|
@@ -200,7 +200,7 @@ function fetch (input, init = undefined) {
|
|
|
200
200
|
|
|
201
201
|
// 4. Abort the fetch() call with p, request, responseObject,
|
|
202
202
|
// and requestObject’s signal’s abort reason.
|
|
203
|
-
abortFetch(p, request, realResponse, requestObject.signal.reason)
|
|
203
|
+
abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller)
|
|
204
204
|
}
|
|
205
205
|
)
|
|
206
206
|
|
|
@@ -227,7 +227,7 @@ function fetch (input, init = undefined) {
|
|
|
227
227
|
// 2. Abort the fetch() call with p, request, responseObject, and
|
|
228
228
|
// deserializedError.
|
|
229
229
|
|
|
230
|
-
abortFetch(p, request, responseObject, controller.serializedAbortReason)
|
|
230
|
+
abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
|
|
231
231
|
return
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -327,7 +327,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
|
|
|
327
327
|
const markResourceTiming = performance.markResourceTiming
|
|
328
328
|
|
|
329
329
|
// https://fetch.spec.whatwg.org/#abort-fetch
|
|
330
|
-
function abortFetch (p, request, responseObject, error) {
|
|
330
|
+
function abortFetch (p, request, responseObject, error, controller /* undici-specific */) {
|
|
331
331
|
// 1. Reject promise with error.
|
|
332
332
|
if (p) {
|
|
333
333
|
// We might have already resolved the promise at this stage
|
|
@@ -357,13 +357,7 @@ function abortFetch (p, request, responseObject, error) {
|
|
|
357
357
|
// 5. If response’s body is not null and is readable, then error response’s
|
|
358
358
|
// body with error.
|
|
359
359
|
if (response.body?.stream != null && isReadable(response.body.stream)) {
|
|
360
|
-
|
|
361
|
-
if (err.code === 'ERR_INVALID_STATE') {
|
|
362
|
-
// Node bug?
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
throw err
|
|
366
|
-
})
|
|
360
|
+
controller.error(error)
|
|
367
361
|
}
|
|
368
362
|
}
|
|
369
363
|
|
|
@@ -2308,6 +2302,41 @@ async function httpNetworkFetch (
|
|
|
2308
2302
|
reject(error)
|
|
2309
2303
|
},
|
|
2310
2304
|
|
|
2305
|
+
onRequestUpgrade (_controller, status, headers, socket) {
|
|
2306
|
+
// We need to support 200 for websocket over h2 as per RFC-8441
|
|
2307
|
+
// Absence of session means H1
|
|
2308
|
+
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
|
|
2309
|
+
return false
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const headersList = new HeadersList()
|
|
2313
|
+
|
|
2314
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2315
|
+
if (value == null) {
|
|
2316
|
+
continue
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
const headerName = name.toLowerCase()
|
|
2320
|
+
|
|
2321
|
+
if (Array.isArray(value)) {
|
|
2322
|
+
for (const entry of value) {
|
|
2323
|
+
headersList.append(headerName, String(entry), true)
|
|
2324
|
+
}
|
|
2325
|
+
} else {
|
|
2326
|
+
headersList.append(headerName, String(value), true)
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
resolve({
|
|
2331
|
+
status,
|
|
2332
|
+
statusText: STATUS_CODES[status],
|
|
2333
|
+
headersList,
|
|
2334
|
+
socket
|
|
2335
|
+
})
|
|
2336
|
+
|
|
2337
|
+
return true
|
|
2338
|
+
},
|
|
2339
|
+
|
|
2311
2340
|
onUpgrade (status, rawHeaders, socket) {
|
|
2312
2341
|
// We need to support 200 for websocket over h2 as per RFC-8441
|
|
2313
2342
|
// Absence of session means H1
|
|
@@ -242,7 +242,8 @@ class Response {
|
|
|
242
242
|
const clonedResponse = cloneResponse(this.#state)
|
|
243
243
|
|
|
244
244
|
// Note: To re-register because of a new stream.
|
|
245
|
-
|
|
245
|
+
// Don't set finalizers other than for fetch responses.
|
|
246
|
+
if (this.#state.urlList.length !== 0 && this.#state.body?.stream) {
|
|
246
247
|
streamRegistry.register(this, new WeakRef(this.#state.body.stream))
|
|
247
248
|
}
|
|
248
249
|
|
package/lib/web/fetch/util.js
CHANGED
|
@@ -1439,7 +1439,7 @@ function hasAuthenticationEntry (request) {
|
|
|
1439
1439
|
*/
|
|
1440
1440
|
function includesCredentials (url) {
|
|
1441
1441
|
// A URL includes credentials if its username or password is not the empty string.
|
|
1442
|
-
return !!(url.username
|
|
1442
|
+
return !!(url.username || url.password)
|
|
1443
1443
|
}
|
|
1444
1444
|
|
|
1445
1445
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.22.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -71,29 +71,29 @@
|
|
|
71
71
|
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
|
|
72
72
|
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:fetch && npm run test:node-fetch && npm run test:infra && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
|
|
73
73
|
"test:javascript:without-intl": "npm run test:javascript:no-jest",
|
|
74
|
-
"test:busboy": "borp -p \"test/busboy/*.js\"",
|
|
75
|
-
"test:cache": "borp -p \"test/cache/*.js\"",
|
|
76
|
-
"test:cache-interceptor": "borp -p \"test/cache-interceptor/*.js\"",
|
|
74
|
+
"test:busboy": "borp --timeout 180000 -p \"test/busboy/*.js\"",
|
|
75
|
+
"test:cache": "borp --timeout 180000 -p \"test/cache/*.js\"",
|
|
76
|
+
"test:cache-interceptor": "borp --timeout 180000 -p \"test/cache-interceptor/*.js\"",
|
|
77
77
|
"test:cache-interceptor:sqlite": "cross-env NODE_OPTIONS=--experimental-sqlite npm run test:cache-interceptor",
|
|
78
|
-
"test:cookies": "borp -p \"test/cookie/*.js\"",
|
|
79
|
-
"test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"",
|
|
78
|
+
"test:cookies": "borp --timeout 180000 -p \"test/cookie/*.js\"",
|
|
79
|
+
"test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"",
|
|
80
80
|
"test:fuzzing": "node test/fuzzing/fuzzing.test.js",
|
|
81
81
|
"test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
|
|
82
|
-
"test:subresource-integrity": "borp -p \"test/subresource-integrity/*.js\"",
|
|
82
|
+
"test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"",
|
|
83
83
|
"test:h2": "npm run test:h2:core && npm run test:h2:fetch",
|
|
84
|
-
"test:h2:core": "borp -p \"test/+(http2|h2)*.js\"",
|
|
85
|
-
"test:h2:fetch": "npm run build:node && borp -p \"test/fetch/http2*.js\"",
|
|
86
|
-
"test:infra": "borp -p \"test/infra/*.js\"",
|
|
87
|
-
"test:interceptors": "borp -p \"test/interceptors/*.js\"",
|
|
84
|
+
"test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"",
|
|
85
|
+
"test:h2:fetch": "npm run build:node && borp --timeout 180000 -p \"test/fetch/http2*.js\"",
|
|
86
|
+
"test:infra": "borp --timeout 180000 -p \"test/infra/*.js\"",
|
|
87
|
+
"test:interceptors": "borp --timeout 180000 -p \"test/interceptors/*.js\"",
|
|
88
88
|
"test:jest": "cross-env NODE_V8_COVERAGE= jest",
|
|
89
|
-
"test:unit": "borp --expose-gc -p \"test/*.js\"",
|
|
90
|
-
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
|
|
91
|
-
"test:node-test": "borp -p \"test/node-test/**/*.js\"",
|
|
92
|
-
"test:tdd": "borp --expose-gc -p \"test/*.js\"",
|
|
93
|
-
"test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
|
|
89
|
+
"test:unit": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
|
|
90
|
+
"test:node-fetch": "borp --timeout 180000 -p \"test/node-fetch/**/*.js\"",
|
|
91
|
+
"test:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\"",
|
|
92
|
+
"test:tdd": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
|
|
93
|
+
"test:tdd:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\" -w",
|
|
94
94
|
"test:typescript": "tsd && tsc test/imports/undici-import.ts --typeRoots ./types --noEmit && tsc ./types/*.d.ts --noEmit --typeRoots ./types",
|
|
95
|
-
"test:webidl": "borp -p \"test/webidl/*.js\"",
|
|
96
|
-
"test:websocket": "borp -p \"test/websocket/**/*.js\"",
|
|
95
|
+
"test:webidl": "borp --timeout 180000 -p \"test/webidl/*.js\"",
|
|
96
|
+
"test:websocket": "borp --timeout 180000 -p \"test/websocket/**/*.js\"",
|
|
97
97
|
"test:websocket:autobahn": "node test/autobahn/client.js",
|
|
98
98
|
"test:websocket:autobahn:report": "node test/autobahn/report.js",
|
|
99
99
|
"test:wpt:setup": "node test/web-platform-tests/wpt-runner.mjs setup",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"c8": "^10.0.0",
|
|
120
120
|
"cross-env": "^10.0.0",
|
|
121
121
|
"dns-packet": "^5.4.0",
|
|
122
|
-
"esbuild": "^0.
|
|
122
|
+
"esbuild": "^0.27.3",
|
|
123
123
|
"eslint": "^9.9.0",
|
|
124
124
|
"fast-check": "^4.1.1",
|
|
125
125
|
"husky": "^9.0.7",
|
package/types/client.d.ts
CHANGED
|
@@ -102,6 +102,11 @@ export declare namespace Client {
|
|
|
102
102
|
* @default 524288
|
|
103
103
|
*/
|
|
104
104
|
connectionWindowSize?: number;
|
|
105
|
+
/**
|
|
106
|
+
* @description Time interval between PING frames dispatch
|
|
107
|
+
* @default 60000
|
|
108
|
+
*/
|
|
109
|
+
pingInterval?: number;
|
|
105
110
|
}
|
|
106
111
|
export interface SocketInfo {
|
|
107
112
|
localAddress?: string
|