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 = utf8DecodeBytes(Buffer.from(body))
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.
@@ -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: url.href.slice(url.href.indexOf(url.host) + url.host.length, url.hash.length ? -url.hash.length : undefined),
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.24.3",
3
+ "version": "7.24.5",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {