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: 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 }; }