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
43 changes: 43 additions & 0 deletions deps/undici/src/docs/docs/best-practices/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,46 @@ const agent = new Agent({

setGlobalDispatcher(agent)
```

## Guarding against unexpected disconnects

Undici's `Client` automatically reconnects after a socket error. This means
a test can silently disconnect, reconnect, and still pass. Unfortunately,
this could mask bugs like unexpected parser errors or protocol violations.
To catch these silent reconnections, add a disconnect guard after creating
a `Client`:

```js
const { Client } = require('undici')
const { test, after } = require('node:test')
const { tspl } = require('@matteo.collina/tspl')

test('example with disconnect guard', async (t) => {
t = tspl(t, { plan: 1 })

const client = new Client('http://localhost:3000')
after(() => client.close())

client.on('disconnect', () => {
if (!client.closed && !client.destroyed) {
t.fail('unexpected disconnect')
}
})

// ... test logic ...
})
```

`client.close()` and `client.destroy()` both emit `'disconnect'` events, but
those are expected. The guard only fails when a disconnect happens during the
active test (i.e., `!client.closed && !client.destroyed` is true).

Skip the guard for tests where a disconnect is expected behavior, such as:

- Signal aborts (`signal.emit('abort')`, `ac.abort()`)
- Server-side destruction (`res.destroy()`, `req.socket.destroy()`)
- Client-side body destruction mid-stream (`data.body.destroy()`)
- Timeout errors (`HeadersTimeoutError`, `BodyTimeoutError`)
- Successful upgrades (the socket is detached from the `Client`)
- Retry/reconnect tests where the disconnect triggers the retry
- HTTP parser errors from malformed responses (`HTTPParserError`)
10 changes: 9 additions & 1 deletion deps/undici/src/lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,18 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
staleIfError = staleAt + (cacheControlDirectives['stale-if-error'] * 1000)
}

if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
if (cacheControlDirectives.immutable && staleWhileRevalidate === -Infinity && staleIfError === -Infinity) {
immutable = now + 31536000000
}

// When no stale directives or immutable flag, add a revalidation buffer
// equal to the freshness lifetime so the entry survives past staleAt long
// enough to be revalidated instead of silently disappearing.
if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) {
const freshnessLifetime = staleAt - now
return staleAt + freshnessLifetime
}

return Math.max(staleAt, staleWhileRevalidate, staleIfError, immutable)
}

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.24.4 build:wasm
> undici@7.24.5 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
4 changes: 2 additions & 2 deletions deps/undici/src/lib/web/fetch/formdata-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ const { makeEntry } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { isomorphicDecode } = require('../infra')
const { utf8DecodeBytes } = require('../../encoding')

const dd = Buffer.from('--')
const decoder = new TextDecoder()
const decoderIgnoreBOM = new TextDecoder('utf-8', { ignoreBOM: true })

/**
* @param {string} chars
Expand Down Expand Up @@ -188,7 +188,7 @@ function multipartFormDataParser (input, mimeType) {
// 5.11. Otherwise:

// 5.11.1. Let value be the UTF-8 decoding without BOM of body.
value = utf8DecodeBytes(Buffer.from(body))
value = decoderIgnoreBOM.decode(Buffer.from(body))
}

// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
Expand Down
Loading
Loading