Skip to content

feat!: update undici to v8 (Node.js >=22.19), configurable request timeouts - #400

Merged
joewiz merged 4 commits into
eXist-db:mainfrom
line-o:feat/undici-8
Sep 12, 2026
Merged

joewiz merged 4 commits into
eXist-db:mainfrom
line-o:feat/undici-8

Conversation

@line-o

@line-o line-o commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

Closes #398, closes #401. Supersedes #392.

Three commits, meant to be merged as they are (not squashed) so semantic-release sees the breaking change, the feature and the fix.

1. feat!: require Node.js 22.19 and update undici to v8

  • undici ^8.10.2, engines >=22.19.0 (not ^22.19.0, which would exclude Node 24 and 26)
  • CI matrix [22, 24, 26], Node 20 dropped
  • undici 8 fails on import under Node 20 (TypeError: webidl.util.markAsUncloneable is not a function, see the CI of chore: bump undici from 7.25.0 to 8.5.0 #392), so this is a real breaking change
  • undici 8's breaking changes checked against our usage: legacy handler wrappers are gone (our interceptors only pass the handler through), non-real Blob bodies are rejected (not used), interceptors.responseError is unchanged
  • HTTP/2 is now negotiated by default: eXist 6 answers with HTTP/1.1, eXist 7 selects h2 on its TLS port, and REST and XML-RPC both work over it

2. feat: make request timeouts configurable, wait indefinitely by default

undici aborts a request after 300 seconds without response headers or without a body chunk, which cut off long running queries and package installations.

  • connection option timeout in milliseconds, 0 waits indefinitely; the default is now 0
  • readOptionsFromEnv reads EXISTDB_TIMEOUT
  • per call: timeout in the options of queries.execute, read, readAll and REST post overrides the connection timeout and is not sent to the database; methodCall(name, params, { timeout })
  • app.upload, install, deploy and remove never time out
  • documented in the README
  • REST post no longer deletes attribute options from the object passed in

3. fix(rest): send non-ASCII strings with put and post

put and post set content-length to body.length, which counts characters, not bytes. Any non-ASCII string failed with UND_ERR_REQ_CONTENT_LENGTH_MISMATCH, with undici 7 as well. undici derives the header from the body itself, so it is no longer set by hand. Streams were already sent without it.

Testing

Locally against existdb/existdb:6.4.0:

Node suite
22.19.0 123/123
24.10.0 123/123
26.8.1 123/123

New specs: spec/tests/timeout.js (24 tests) and a non-ASCII block in spec/tests/rest.js (3 tests).

Against existdb/existdb:latest (7.0.0-SNAPSHOT, h2): timeout.js 24/24 and the non-ASCII REST tests pass. The full suite fails 7 tests there, identically with undici 7 on main: eXist 7 behaviour changes (EXPATH007 error message, XML declaration serialized by default, REST start/max/cache), see #339.

The release cells in CI have failed on every PR since June (e.g. #391, #394), independent of this change.

Downstream

xst and gulp-exist depend on ^7.0.0 and keep working until they opt in. Adopting 8.0.0 requires Node.js >=22.19 there as well.

🤖 Generated with Claude Code

https://claude.ai/code/session_016bdrW9vpnvJkeBtS1UZ92E

line-o and others added 3 commits September 10, 2026 20:45
undici 8 declares engines node >=22.19.0 and fails on import under
Node 20 (TypeError: webidl.util.markAsUncloneable is not a function).
Node 20 is dropped from engines and the CI matrix; Node 26 is added.

Reviewed the undici 8 breaking changes against our usage: the legacy
handler wrappers are gone (our interceptors only pass the handler
through), non-real Blob bodies are rejected (we never send one), and
interceptors.responseError keeps its behaviour.

HTTP/2 is now negotiated by default via ALPN. eXist 6 answers with
HTTP/1.1, eXist 7 selects h2 on its TLS port; REST and XML-RPC both work
over h2, and the suite fails the same tests against 7.0.0-SNAPSHOT with
undici 7 and undici 8.

BREAKING CHANGE: Node.js >=22.19.0 is required.

Closes eXist-db#398, supersedes eXist-db#392

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bdrW9vpnvJkeBtS1UZ92E
undici aborts a request after 300 seconds without response headers or
without a body chunk. Long running queries and package installations
regularly take longer and failed with UND_ERR_HEADERS_TIMEOUT.

- connection option `timeout` (milliseconds, 0 waits indefinitely) sets
  undici's headersTimeout and bodyTimeout; the default is now 0
- readOptionsFromEnv reads EXISTDB_TIMEOUT
- methodCall accepts per-call options; queries.execute, read and readAll
  and REST post take `timeout` from their options and do not send it to
  the database
- app.upload, install, deploy and remove never time out

REST post no longer deletes attribute options from the object passed in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bdrW9vpnvJkeBtS1UZ92E
put and post set content-length to body.length, which counts the
characters of a string, not its bytes. Any non-ASCII character made
undici reject the request with UND_ERR_REQ_CONTENT_LENGTH_MISMATCH.

undici derives content-length from the body itself, in bytes, for
strings and buffers alike, so the header is no longer set by hand.
Streams were already sent without it; their separate branch in put is
gone because it is now identical to the default one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bdrW9vpnvJkeBtS1UZ92E
@codacy-production

codacy-production Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 8 minor

Alerts:
⚠ 8 issues (≤ 0 issues of at least minor severity)

Results:
8 new issues

Category Results
CodeStyle 8 minor

View in Codacy

🟢 Metrics 34 complexity · 0 duplication

Metric Results
Complexity 34
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

- build the attributes and properties of a REST query from
  Object.entries instead of looking up and deleting computed keys
  (security/detect-object-injection, no-dynamic-delete); attributes now
  follow the order of the options, which XML does not care about
- drop the unused `_` parameter in app.upload
- no bold text in the new timeout example of the README (MD050)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bdrW9vpnvJkeBtS1UZ92E
@joewiz
joewiz merged commit 2aa6edd into eXist-db:main Sep 12, 2026
14 of 23 checks passed
@line-o
line-o deleted the feat/undici-8 branch September 13, 2026 07:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

requests are aborted after 300 seconds (undici default timeouts) migrate to undici 8, require Node >= 22.19

2 participants