undici 7.24.3 → 7.24.5
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.
|
@@ -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`)
|
|
@@ -493,10 +493,18 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
|
|
|
493
493
|
staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
496
|
+
if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
|
|
497
497
|
immutable = now + 31536000000
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// When no stale directives or immutable flag, add a revalidation buffer
|
|
501
|
+
// equal to the freshness lifetime so the entry survives past staleAt long
|
|
502
|
+
// enough to be revalidated instead of silently disappearing.
|
|
503
|
+
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
|
|
504
|
+
const freshnessLifetime = staleAt - now
|
|
505
|
+
return staleAt + freshnessLifetime
|
|
506
|
+
}
|
|
507
|
+
|
|
500
508
|
return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
|
|
501
509
|
}
|
|
502
510
|
|
|
@@ -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.
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -2132,9 +2132,12 @@ async function httpNetworkFetch (
|
|
|
2132
2132
|
/** @type {import('../../..').Agent} */
|
|
2133
2133
|
const agent = fetchParams.controller.dispatcher
|
|
2134
2134
|
|
|
2135
|
+
const path = url.pathname + url.search
|
|
2136
|
+
const hasTrailingQuestionMark = url.search.length === 0 && url.href[url.href.length - url.hash.length - 1] === '?'
|
|
2137
|
+
|
|
2135
2138
|
return new Promise((resolve, reject) => agent.dispatch(
|
|
2136
2139
|
{
|
|
2137
|
-
path:
|
|
2140
|
+
path: hasTrailingQuestionMark ? `${path}?` : path,
|
|
2138
2141
|
origin: url.origin,
|
|
2139
2142
|
method: request.method,
|
|
2140
2143
|
body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
|