From 37ea96d4ef420ee1f0efd76b0f8fc6a2ea5e6968 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 24 Jul 2026 22:57:50 -0400 Subject: [PATCH 1/2] Test on Node.js 26.x. --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 03191b3..96d5d7d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -27,7 +27,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [22.x, 24.x] + node-version: [22.x, 24.x, 26.x] steps: - uses: actions/checkout@v7 with: From fa05b8e30e228692be9a7f757a0acdf36cc46663 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Jul 2026 02:47:00 +0000 Subject: [PATCH 2/2] Fix agent conversion on newer node versions. A dispatcher is only usable by the undici instance that created it. Node bundles its own undici (node 22: 6.x, node 24: 7.x, node 26: 8.x) and the dispatcher handler interface changed between majors, so passing an installed-undici dispatcher to `globalThis.fetch` fails. undici 7 still accepted 6.x style handlers, but undici 8 does not, breaking node 26 with `UND_ERR_INVALID_ARG` ("invalid onError method"). Call undici's own `fetch` instead so the dispatcher and the `fetch` using it always come from the same module. This works on any node version and with any installed undici, rather than pinning undici to whichever node happens to match. undici's `fetch` does not accept the global `Request` that `ky` passes, so unpack it into a url and init pair. Co-Authored-By: Claude Opus 5 (1M context) --- lib/agentCompatibility.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/agentCompatibility.js b/lib/agentCompatibility.js index cd1b93c..504fce6 100644 --- a/lib/agentCompatibility.js +++ b/lib/agentCompatibility.js @@ -1,7 +1,7 @@ /*! * Copyright (c) 2022 Digital Bazaar, Inc. All rights reserved. */ -import {Agent} from 'undici'; +import {Agent, fetch as undiciFetch} from 'undici'; // as long as an agent has a reference to it, its associated dispatcher will // be kept in this cache for reuse @@ -34,10 +34,39 @@ export function convertAgent(options) { // create fetch override uses custom `dispatcher`; since `ky` does not pass // the dispatcher option through to `fetch`, we must use this override +// +// this uses undici's own `fetch` rather than `globalThis.fetch`. a dispatcher +// is only usable by the undici that created it: node bundles its own undici +// (node 22: 6.x, node 24: 7.x, node 26: 8.x) and the handler interface changed +// between majors, so handing an installed-undici dispatcher to node's built-in +// `fetch` throws `UND_ERR_INVALID_ARG` ("invalid onError method" on node 26). +// pairing the dispatcher with the `fetch` from the same module keeps the two +// in sync on any node version, whichever undici is installed function createFetch(dispatcher) { return function fetch(...args) { dispatcher = (args[1] && args[1].dispatcher) || dispatcher; args[1] = {...args[1], dispatcher}; - return globalThis.fetch(...args); + // `ky` builds a global `Request`, which undici's `fetch` does not accept + // as one of its own; unpack it into a url and init pair instead + if(args[0] instanceof globalThis.Request) { + args[1] = {..._requestToInit(args[0]), ...args[1]}; + args[0] = args[0].url; + } + return undiciFetch(...args); + }; +} + +// converts a `Request` into an equivalent `fetch` init object +function _requestToInit(request) { + const {body} = request; + return { + method: request.method, + // an entry list is understood by every `Headers` implementation + headers: [...request.headers], + body, + // undici requires `duplex` whenever a stream body is sent + duplex: body ? 'half' : undefined, + signal: request.signal, + redirect: request.redirect }; }