undici 7.24.4 → 7.24.6
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/README.md +56 -0
- package/docs/docs/api/DiagnosticsChannel.md +8 -6
- package/docs/docs/api/Fetch.md +8 -0
- package/docs/docs/api/GlobalInstallation.md +48 -0
- package/docs/docs/best-practices/undici-vs-builtin-fetch.md +90 -3
- package/docs/docs/best-practices/writing-tests.md +43 -0
- package/lib/core/diagnostics.js +6 -4
- package/lib/core/request.js +12 -4
- package/lib/core/util.js +31 -11
- package/lib/dispatcher/client.js +58 -53
- package/lib/handler/cache-handler.js +22 -5
- package/lib/mock/mock-symbols.js +2 -1
- package/lib/mock/mock-utils.js +9 -3
- package/lib/web/fetch/formdata-parser.js +2 -2
- package/lib/web/websocket/websocket.js +13 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,6 +154,57 @@ const { statusCode, body } = await request('https://api.example.com/data');
|
|
|
154
154
|
const data = await body.json();
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### Keep `fetch` and `FormData` together
|
|
158
|
+
|
|
159
|
+
When you send a `FormData` body, keep `fetch` and `FormData` from the same
|
|
160
|
+
implementation.
|
|
161
|
+
|
|
162
|
+
Use one of these patterns:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
// Built-in globals
|
|
166
|
+
const body = new FormData()
|
|
167
|
+
body.set('name', 'some')
|
|
168
|
+
await fetch('https://example.com', {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
// undici module imports
|
|
176
|
+
import { fetch, FormData } from 'undici'
|
|
177
|
+
|
|
178
|
+
const body = new FormData()
|
|
179
|
+
body.set('name', 'some')
|
|
180
|
+
await fetch('https://example.com', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you want the installed `undici` package to provide the globals, call
|
|
187
|
+
`install()` first:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { install } from 'undici'
|
|
191
|
+
|
|
192
|
+
install()
|
|
193
|
+
|
|
194
|
+
const body = new FormData()
|
|
195
|
+
body.set('name', 'some')
|
|
196
|
+
await fetch('https://example.com', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
|
|
203
|
+
`FormData` implementations with undici's versions, so they all match.
|
|
204
|
+
|
|
205
|
+
Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
|
|
206
|
+
with the built-in global `fetch()`.
|
|
207
|
+
|
|
157
208
|
### Version Compatibility
|
|
158
209
|
|
|
159
210
|
You can check which version of undici is bundled with your Node.js version:
|
|
@@ -263,6 +314,11 @@ The `install()` function adds the following classes to `globalThis`:
|
|
|
263
314
|
- `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events
|
|
264
315
|
- `EventSource` - Server-sent events client
|
|
265
316
|
|
|
317
|
+
When you call `install()`, these globals come from the same undici
|
|
318
|
+
implementation. For example, global `fetch` and global `FormData` will both be
|
|
319
|
+
undici's versions, which is the recommended setup if you want to use undici
|
|
320
|
+
through globals.
|
|
321
|
+
|
|
266
322
|
This is useful for:
|
|
267
323
|
- Polyfilling environments that don't have fetch
|
|
268
324
|
- Ensuring consistent fetch behavior across different Node.js versions
|
|
@@ -182,22 +182,24 @@ diagnosticsChannel.channel('undici:websocket:open').subscribe(({
|
|
|
182
182
|
console.log(websocket) // the WebSocket instance
|
|
183
183
|
|
|
184
184
|
// Handshake response details
|
|
185
|
-
console.log(handshakeResponse.status) // 101 for
|
|
186
|
-
console.log(handshakeResponse.statusText) // 'Switching Protocols'
|
|
185
|
+
console.log(handshakeResponse.status) // 101 for HTTP/1.1, 200 for HTTP/2 extended CONNECT
|
|
186
|
+
console.log(handshakeResponse.statusText) // 'Switching Protocols' for HTTP/1.1, commonly 'OK' for HTTP/2 in Node.js
|
|
187
187
|
console.log(handshakeResponse.headers) // Object containing response headers
|
|
188
188
|
})
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
### Handshake Response Object
|
|
192
192
|
|
|
193
|
-
The `handshakeResponse` object contains the HTTP response that
|
|
193
|
+
The `handshakeResponse` object contains the HTTP response that established the WebSocket connection:
|
|
194
194
|
|
|
195
|
-
- `status` (number): The HTTP status code (101 for
|
|
196
|
-
- `statusText` (string): The HTTP status message ('Switching Protocols' for
|
|
195
|
+
- `status` (number): The HTTP status code (`101` for HTTP/1.1 upgrade, `200` for HTTP/2 extended CONNECT)
|
|
196
|
+
- `statusText` (string): The HTTP status message (`'Switching Protocols'` for HTTP/1.1, commonly `'OK'` for HTTP/2 in Node.js)
|
|
197
197
|
- `headers` (object): The HTTP response headers from the server, including:
|
|
198
|
+
- `sec-websocket-accept` and other WebSocket-related headers
|
|
198
199
|
- `upgrade: 'websocket'`
|
|
199
200
|
- `connection: 'upgrade'`
|
|
200
|
-
|
|
201
|
+
|
|
202
|
+
The `upgrade` and `connection` headers are only present for HTTP/1.1 handshakes.
|
|
201
203
|
|
|
202
204
|
This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection.
|
|
203
205
|
|
package/docs/docs/api/Fetch.md
CHANGED
|
@@ -10,6 +10,14 @@ This API is implemented as per the standard, you can find documentation on [MDN]
|
|
|
10
10
|
|
|
11
11
|
If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored.
|
|
12
12
|
|
|
13
|
+
When you use `FormData` as a request body, keep `fetch` and `FormData` from the
|
|
14
|
+
same implementation. Use the built-in global `FormData` with the built-in
|
|
15
|
+
global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`.
|
|
16
|
+
|
|
17
|
+
If you want the installed `undici` package to provide the globals, call
|
|
18
|
+
[`install()`](/docs/api/GlobalInstallation.md) so `fetch`, `Headers`,
|
|
19
|
+
`Response`, `Request`, and `FormData` are installed together as a matching set.
|
|
20
|
+
|
|
13
21
|
## Response
|
|
14
22
|
|
|
15
23
|
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
|
@@ -43,6 +43,54 @@ The `install()` function adds the following classes to `globalThis`:
|
|
|
43
43
|
| `MessageEvent` | WebSocket message event |
|
|
44
44
|
| `EventSource` | Server-sent events client |
|
|
45
45
|
|
|
46
|
+
## Using `FormData` with `fetch`
|
|
47
|
+
|
|
48
|
+
If you send a `FormData` body, use matching implementations for `fetch` and
|
|
49
|
+
`FormData`.
|
|
50
|
+
|
|
51
|
+
These two patterns are safe:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// Built-in globals from Node.js
|
|
55
|
+
const body = new FormData()
|
|
56
|
+
await fetch('https://example.com', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
// Globals installed from the undici package
|
|
64
|
+
import { install } from 'undici'
|
|
65
|
+
|
|
66
|
+
install()
|
|
67
|
+
|
|
68
|
+
const body = new FormData()
|
|
69
|
+
await fetch('https://example.com', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData`
|
|
76
|
+
all come from the installed `undici` package, so they work as a matching set.
|
|
77
|
+
|
|
78
|
+
If you do not want to install globals, import both from `undici` instead:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import { fetch, FormData } from 'undici'
|
|
82
|
+
|
|
83
|
+
const body = new FormData()
|
|
84
|
+
await fetch('https://example.com', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
|
|
91
|
+
with the built-in global `fetch()`. Keeping them paired avoids surprising
|
|
92
|
+
multipart behavior across Node.js and undici versions.
|
|
93
|
+
|
|
46
94
|
## Use Cases
|
|
47
95
|
|
|
48
96
|
Global installation is useful for:
|
|
@@ -19,6 +19,93 @@ When you install undici from npm, you get the full library with all of its
|
|
|
19
19
|
additional APIs, and potentially a newer release than what your Node.js version
|
|
20
20
|
bundles.
|
|
21
21
|
|
|
22
|
+
## Keep `fetch` and `FormData` from the same implementation
|
|
23
|
+
|
|
24
|
+
When you send a `FormData` body, keep `fetch` and `FormData` together from the
|
|
25
|
+
same implementation.
|
|
26
|
+
|
|
27
|
+
Use one of these patterns:
|
|
28
|
+
|
|
29
|
+
### Built-in globals
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const body = new FormData()
|
|
33
|
+
body.set('name', 'some')
|
|
34
|
+
body.set('someOtherProperty', '8000')
|
|
35
|
+
|
|
36
|
+
await fetch('https://example.com', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `undici` module imports
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { fetch, FormData } from 'undici'
|
|
46
|
+
|
|
47
|
+
const body = new FormData()
|
|
48
|
+
body.set('name', 'some')
|
|
49
|
+
body.set('someOtherProperty', '8000')
|
|
50
|
+
|
|
51
|
+
await fetch('https://example.com', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `undici.install()` globals
|
|
58
|
+
|
|
59
|
+
If you want the installed `undici` package to provide the globals, call
|
|
60
|
+
[`install()`](/docs/api/GlobalInstallation.md):
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { install } from 'undici'
|
|
64
|
+
|
|
65
|
+
install()
|
|
66
|
+
|
|
67
|
+
const body = new FormData()
|
|
68
|
+
body.set('name', 'some')
|
|
69
|
+
body.set('someOtherProperty', '8000')
|
|
70
|
+
|
|
71
|
+
await fetch('https://example.com', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
|
|
78
|
+
`FormData` implementations with undici's versions, and also installs undici's
|
|
79
|
+
`WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource`
|
|
80
|
+
globals.
|
|
81
|
+
|
|
82
|
+
Avoid mixing implementations in the same request, for example:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { fetch } from 'undici'
|
|
86
|
+
|
|
87
|
+
const body = new FormData()
|
|
88
|
+
|
|
89
|
+
await fetch('https://example.com', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { FormData } from 'undici'
|
|
97
|
+
|
|
98
|
+
const body = new FormData()
|
|
99
|
+
|
|
100
|
+
await fetch('https://example.com', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Those combinations may behave differently across Node.js and undici versions.
|
|
107
|
+
Using matching pairs keeps multipart handling predictable.
|
|
108
|
+
|
|
22
109
|
## When you do NOT need to install undici
|
|
23
110
|
|
|
24
111
|
If all of the following are true, you can rely on the built-in globals and skip
|
|
@@ -119,12 +206,12 @@ You can always check the exact bundled version at runtime with
|
|
|
119
206
|
`process.versions.undici`.
|
|
120
207
|
|
|
121
208
|
Installing undici from npm does not replace the built-in globals. If you want
|
|
122
|
-
your installed version to
|
|
123
|
-
[`
|
|
209
|
+
your installed version to replace the global `fetch` and related classes, use
|
|
210
|
+
[`install()`](/docs/api/GlobalInstallation.md). Otherwise, import `fetch`
|
|
124
211
|
directly from `'undici'`:
|
|
125
212
|
|
|
126
213
|
```js
|
|
127
|
-
import { fetch } from 'undici'
|
|
214
|
+
import { fetch } from 'undici' // uses your installed version, not the built-in
|
|
128
215
|
```
|
|
129
216
|
|
|
130
217
|
## Further reading
|
|
@@ -18,3 +18,46 @@ const agent = new Agent({
|
|
|
18
18
|
|
|
19
19
|
setGlobalDispatcher(agent)
|
|
20
20
|
```
|
|
21
|
+
|
|
22
|
+
## Guarding against unexpected disconnects
|
|
23
|
+
|
|
24
|
+
Undici's `Client` automatically reconnects after a socket error. This means
|
|
25
|
+
a test can silently disconnect, reconnect, and still pass. Unfortunately,
|
|
26
|
+
this could mask bugs like unexpected parser errors or protocol violations.
|
|
27
|
+
To catch these silent reconnections, add a disconnect guard after creating
|
|
28
|
+
a `Client`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { Client } = require('undici')
|
|
32
|
+
const { test, after } = require('node:test')
|
|
33
|
+
const { tspl } = require('@matteo.collina/tspl')
|
|
34
|
+
|
|
35
|
+
test('example with disconnect guard', async (t) => {
|
|
36
|
+
t = tspl(t, { plan: 1 })
|
|
37
|
+
|
|
38
|
+
const client = new Client('http://localhost:3000')
|
|
39
|
+
after(() => client.close())
|
|
40
|
+
|
|
41
|
+
client.on('disconnect', () => {
|
|
42
|
+
if (!client.closed && !client.destroyed) {
|
|
43
|
+
t.fail('unexpected disconnect')
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// ... test logic ...
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`client.close()` and `client.destroy()` both emit `'disconnect'` events, but
|
|
52
|
+
those are expected. The guard only fails when a disconnect happens during the
|
|
53
|
+
active test (i.e., `!client.closed && !client.destroyed` is true).
|
|
54
|
+
|
|
55
|
+
Skip the guard for tests where a disconnect is expected behavior, such as:
|
|
56
|
+
|
|
57
|
+
- Signal aborts (`signal.emit('abort')`, `ac.abort()`)
|
|
58
|
+
- Server-side destruction (`res.destroy()`, `req.socket.destroy()`)
|
|
59
|
+
- Client-side body destruction mid-stream (`data.body.destroy()`)
|
|
60
|
+
- Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`)
|
|
61
|
+
- Successful upgrades (the socket is detached from the `Client`)
|
|
62
|
+
- Retry/reconnect tests where the disconnect triggers the retry
|
|
63
|
+
- HTTP parser errors from malformed responses (`HTTPParserError`)
|
package/lib/core/diagnostics.js
CHANGED
|
@@ -177,10 +177,12 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) {
|
|
|
177
177
|
|
|
178
178
|
diagnosticsChannel.subscribe('undici:websocket:open',
|
|
179
179
|
evt => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
if (evt.address != null) {
|
|
181
|
+
const { address, port } = evt.address
|
|
182
|
+
debugLog('connection opened %s%s', address, port ? `:${port}` : '')
|
|
183
|
+
} else {
|
|
184
|
+
debugLog('connection opened')
|
|
185
|
+
}
|
|
184
186
|
})
|
|
185
187
|
|
|
186
188
|
diagnosticsChannel.subscribe('undici:websocket:close',
|
package/lib/core/request.js
CHANGED
|
@@ -412,13 +412,21 @@ function processHeader (request, key, val) {
|
|
|
412
412
|
} else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
|
|
413
413
|
throw new InvalidArgumentError(`invalid ${headerName} header`)
|
|
414
414
|
} else if (headerName === 'connection') {
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
// Per RFC 7230 Section 6.1, Connection header can contain
|
|
416
|
+
// a comma-separated list of connection option tokens (header names)
|
|
417
|
+
const value = typeof val === 'string' ? val : null
|
|
418
|
+
if (value === null) {
|
|
417
419
|
throw new InvalidArgumentError('invalid connection header')
|
|
418
420
|
}
|
|
419
421
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
for (const token of value.toLowerCase().split(',')) {
|
|
423
|
+
const trimmed = token.trim()
|
|
424
|
+
if (!isValidHTTPToken(trimmed)) {
|
|
425
|
+
throw new InvalidArgumentError('invalid connection header')
|
|
426
|
+
}
|
|
427
|
+
if (trimmed === 'close') {
|
|
428
|
+
request.reset = true
|
|
429
|
+
}
|
|
422
430
|
}
|
|
423
431
|
} else if (headerName === 'expect') {
|
|
424
432
|
throw new NotSupportedError('expect header not supported')
|
package/lib/core/util.js
CHANGED
|
@@ -440,19 +440,39 @@ function parseHeaders (headers, obj) {
|
|
|
440
440
|
const key = headerNameToString(headers[i])
|
|
441
441
|
let val = obj[key]
|
|
442
442
|
|
|
443
|
-
if (val) {
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
443
|
+
if (val !== undefined) {
|
|
444
|
+
if (!Object.hasOwn(obj, key)) {
|
|
445
|
+
const headersValue = typeof headers[i + 1] === 'string'
|
|
446
|
+
? headers[i + 1]
|
|
447
|
+
: Array.isArray(headers[i + 1])
|
|
448
|
+
? headers[i + 1].map(x => x.toString('latin1'))
|
|
449
|
+
: headers[i + 1].toString('latin1')
|
|
450
|
+
|
|
451
|
+
if (key === '__proto__') {
|
|
452
|
+
Object.defineProperty(obj, key, {
|
|
453
|
+
value: headersValue,
|
|
454
|
+
enumerable: true,
|
|
455
|
+
configurable: true,
|
|
456
|
+
writable: true
|
|
457
|
+
})
|
|
458
|
+
} else {
|
|
459
|
+
obj[key] = headersValue
|
|
460
|
+
}
|
|
453
461
|
} else {
|
|
454
|
-
|
|
462
|
+
if (typeof val === 'string') {
|
|
463
|
+
val = [val]
|
|
464
|
+
obj[key] = val
|
|
465
|
+
}
|
|
466
|
+
val.push(headers[i + 1].toString('latin1'))
|
|
455
467
|
}
|
|
468
|
+
} else {
|
|
469
|
+
const headersValue = typeof headers[i + 1] === 'string'
|
|
470
|
+
? headers[i + 1]
|
|
471
|
+
: Array.isArray(headers[i + 1])
|
|
472
|
+
? headers[i + 1].map(x => x.toString('latin1'))
|
|
473
|
+
: headers[i + 1].toString('latin1')
|
|
474
|
+
|
|
475
|
+
obj[key] = headersValue
|
|
456
476
|
}
|
|
457
477
|
}
|
|
458
478
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -452,65 +452,70 @@ function connect (client) {
|
|
|
452
452
|
})
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
455
|
+
try {
|
|
456
|
+
client[kConnector]({
|
|
457
|
+
host,
|
|
458
|
+
hostname,
|
|
459
|
+
protocol,
|
|
460
|
+
port,
|
|
461
|
+
servername: client[kServerName],
|
|
462
|
+
localAddress: client[kLocalAddress]
|
|
463
|
+
}, (err, socket) => {
|
|
464
|
+
if (err) {
|
|
465
|
+
handleConnectError(client, err, { host, hostname, protocol, port })
|
|
466
|
+
client[kResume]()
|
|
467
|
+
return
|
|
468
|
+
}
|
|
468
469
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
470
|
+
if (client.destroyed) {
|
|
471
|
+
util.destroy(socket.on('error', noop), new ClientDestroyedError())
|
|
472
|
+
client[kResume]()
|
|
473
|
+
return
|
|
474
|
+
}
|
|
474
475
|
|
|
475
|
-
|
|
476
|
+
assert(socket)
|
|
476
477
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
478
|
+
try {
|
|
479
|
+
client[kHTTPContext] = socket.alpnProtocol === 'h2'
|
|
480
|
+
? connectH2(client, socket)
|
|
481
|
+
: connectH1(client, socket)
|
|
482
|
+
} catch (err) {
|
|
483
|
+
socket.destroy().on('error', noop)
|
|
484
|
+
handleConnectError(client, err, { host, hostname, protocol, port })
|
|
485
|
+
client[kResume]()
|
|
486
|
+
return
|
|
487
|
+
}
|
|
487
488
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
489
|
+
client[kConnecting] = false
|
|
490
|
+
|
|
491
|
+
socket[kCounter] = 0
|
|
492
|
+
socket[kMaxRequests] = client[kMaxRequests]
|
|
493
|
+
socket[kClient] = client
|
|
494
|
+
socket[kError] = null
|
|
495
|
+
|
|
496
|
+
if (channels.connected.hasSubscribers) {
|
|
497
|
+
channels.connected.publish({
|
|
498
|
+
connectParams: {
|
|
499
|
+
host,
|
|
500
|
+
hostname,
|
|
501
|
+
protocol,
|
|
502
|
+
port,
|
|
503
|
+
version: client[kHTTPContext]?.version,
|
|
504
|
+
servername: client[kServerName],
|
|
505
|
+
localAddress: client[kLocalAddress]
|
|
506
|
+
},
|
|
507
|
+
connector: client[kConnector],
|
|
508
|
+
socket
|
|
509
|
+
})
|
|
510
|
+
}
|
|
510
511
|
|
|
511
|
-
|
|
512
|
+
client.emit('connect', client[kUrl], [client])
|
|
513
|
+
client[kResume]()
|
|
514
|
+
})
|
|
515
|
+
} catch (err) {
|
|
516
|
+
handleConnectError(client, err, { host, hostname, protocol, port })
|
|
512
517
|
client[kResume]()
|
|
513
|
-
}
|
|
518
|
+
}
|
|
514
519
|
}
|
|
515
520
|
|
|
516
521
|
function handleConnectError (client, err, { host, hostname, protocol, port }) {
|
|
@@ -135,7 +135,7 @@ class CacheHandler {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
|
|
138
|
-
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives)) {
|
|
138
|
+
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
|
|
139
139
|
return downstreamOnHeaders()
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -340,8 +340,9 @@ class CacheHandler {
|
|
|
340
340
|
* @param {number} statusCode
|
|
341
341
|
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
|
|
342
342
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
343
|
+
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
|
|
343
344
|
*/
|
|
344
|
-
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
|
|
345
|
+
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
|
|
345
346
|
// Status code must be final and understood.
|
|
346
347
|
if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) {
|
|
347
348
|
return false
|
|
@@ -372,8 +373,16 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
|
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
// https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
|
|
375
|
-
if (
|
|
376
|
-
if (
|
|
376
|
+
if (reqHeaders?.authorization) {
|
|
377
|
+
if (
|
|
378
|
+
!cacheControlDirectives.public &&
|
|
379
|
+
!cacheControlDirectives['s-maxage'] &&
|
|
380
|
+
!cacheControlDirectives['must-revalidate']
|
|
381
|
+
) {
|
|
382
|
+
return false
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (typeof reqHeaders.authorization !== 'string') {
|
|
377
386
|
return false
|
|
378
387
|
}
|
|
379
388
|
|
|
@@ -493,10 +502,18 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
|
493
502
|
staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
|
|
494
503
|
}
|
|
495
504
|
|
|
496
|
-
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
505
|
+
if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
497
506
|
immutable = now + 31536000000
|
|
498
507
|
}
|
|
499
508
|
|
|
509
|
+
// When no stale directives or immutable flag, add a revalidation buffer
|
|
510
|
+
// equal to the freshness lifetime so the entry survives past staleAt long
|
|
511
|
+
// enough to be revalidated instead of silently disappearing.
|
|
512
|
+
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
|
|
513
|
+
const freshnessLifetime = staleAt - now
|
|
514
|
+
return staleAt + freshnessLifetime
|
|
515
|
+
}
|
|
516
|
+
|
|
500
517
|
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
|
|
501
518
|
}
|
|
502
519
|
|
package/lib/mock/mock-symbols.js
CHANGED
|
@@ -27,5 +27,6 @@ module.exports = {
|
|
|
27
27
|
kMockAgentAddCallHistoryLog: Symbol('mock agent add call history log'),
|
|
28
28
|
kMockAgentIsCallHistoryEnabled: Symbol('mock agent is call history enabled'),
|
|
29
29
|
kMockAgentAcceptsNonStandardSearchParameters: Symbol('mock agent accepts non standard search parameters'),
|
|
30
|
-
kMockCallHistoryAddLog: Symbol('mock call history add log')
|
|
30
|
+
kMockCallHistoryAddLog: Symbol('mock call history add log'),
|
|
31
|
+
kTotalDispatchCount: Symbol('total dispatch count')
|
|
31
32
|
}
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -6,7 +6,8 @@ const {
|
|
|
6
6
|
kMockAgent,
|
|
7
7
|
kOriginalDispatch,
|
|
8
8
|
kOrigin,
|
|
9
|
-
kGetNetConnect
|
|
9
|
+
kGetNetConnect,
|
|
10
|
+
kTotalDispatchCount
|
|
10
11
|
} = require('./mock-symbols')
|
|
11
12
|
const { serializePathWithQuery } = require('../core/util')
|
|
12
13
|
const { STATUS_CODES } = require('node:http')
|
|
@@ -206,6 +207,8 @@ function addMockDispatch (mockDispatches, key, data, opts) {
|
|
|
206
207
|
const replyData = typeof data === 'function' ? { callback: data } : { ...data }
|
|
207
208
|
const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
|
|
208
209
|
mockDispatches.push(newMockDispatch)
|
|
210
|
+
// Track total number of intercepts ever registered for better error messages
|
|
211
|
+
mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1
|
|
209
212
|
return newMockDispatch
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -401,13 +404,16 @@ function buildMockDispatch () {
|
|
|
401
404
|
} catch (error) {
|
|
402
405
|
if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
|
|
403
406
|
const netConnect = agent[kGetNetConnect]()
|
|
407
|
+
const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length
|
|
408
|
+
const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length
|
|
409
|
+
const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined`
|
|
404
410
|
if (netConnect === false) {
|
|
405
|
-
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`)
|
|
411
|
+
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`)
|
|
406
412
|
}
|
|
407
413
|
if (checkNetConnect(netConnect, origin)) {
|
|
408
414
|
originalDispatch.call(this, opts, handler)
|
|
409
415
|
} else {
|
|
410
|
-
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`)
|
|
416
|
+
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`)
|
|
411
417
|
}
|
|
412
418
|
} else {
|
|
413
419
|
throw error
|
|
@@ -6,10 +6,10 @@ const { makeEntry } = require('./formdata')
|
|
|
6
6
|
const { webidl } = require('../webidl')
|
|
7
7
|
const assert = require('node:assert')
|
|
8
8
|
const { isomorphicDecode } = require('../infra')
|
|
9
|
-
const { utf8DecodeBytes } = require('../../encoding')
|
|
10
9
|
|
|
11
10
|
const dd = Buffer.from('--')
|
|
12
11
|
const decoder = new TextDecoder()
|
|
12
|
+
const decoderIgnoreBOM = new TextDecoder('utf-8', { ignoreBOM: true })
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} chars
|
|
@@ -188,7 +188,7 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
188
188
|
// 5.11. Otherwise:
|
|
189
189
|
|
|
190
190
|
// 5.11.1. Let value be the UTF-8 decoding without BOM of body.
|
|
191
|
-
value =
|
|
191
|
+
value = decoderIgnoreBOM.decode(Buffer.from(body))
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
|
|
@@ -25,6 +25,18 @@ const { SendQueue } = require('./sender')
|
|
|
25
25
|
const { WebsocketFrameSend } = require('./frame')
|
|
26
26
|
const { channels } = require('../../core/diagnostics')
|
|
27
27
|
|
|
28
|
+
function getSocketAddress (socket) {
|
|
29
|
+
if (typeof socket?.address === 'function') {
|
|
30
|
+
return socket.address()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof socket?.session?.socket?.address === 'function') {
|
|
34
|
+
return socket.session.socket.address()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
/**
|
|
29
41
|
* @typedef {object} Handler
|
|
30
42
|
* @property {(response: any, extensions?: string[]) => void} onConnectionEstablished
|
|
@@ -491,7 +503,7 @@ class WebSocket extends EventTarget {
|
|
|
491
503
|
// Convert headers to a plain object for the event
|
|
492
504
|
const headers = response.headersList.entries
|
|
493
505
|
channels.open.publish({
|
|
494
|
-
address: response.socket
|
|
506
|
+
address: getSocketAddress(response.socket),
|
|
495
507
|
protocol: this.#protocol,
|
|
496
508
|
extensions: this.#extensions,
|
|
497
509
|
websocket: this,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "7.24.
|
|
3
|
+
"version": "7.24.6",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"node-forge": "^1.3.1",
|
|
130
130
|
"proxy": "^2.1.1",
|
|
131
131
|
"tsd": "^0.33.0",
|
|
132
|
-
"typescript": "^
|
|
132
|
+
"typescript": "^6.0.2",
|
|
133
133
|
"ws": "^8.11.0"
|
|
134
134
|
},
|
|
135
135
|
"engines": {
|