From 2e97e29ab52ffeabd6a7b83ce3186aec0057c682 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:42:30 +0200 Subject: [PATCH] fix(http_fetcher): use undici.fetch when dispatcher is present (#4097) ### What's the problem? The `selfSignedCert` option passes an undici `Agent` as `dispatcher` to `fetch()`. But Node's built-in `fetch()` and undici@8's `Agent` use different internal handler APIs - passing them together throws: ``` invalid onRequestStart method ``` ### What's the fix? When `selfSignedCert` is enabled (i.e. a `dispatcher` is set), use undici's own `fetch()` instead of the global one. For all other requests, keep using `globalThis.fetch`. ```js const fetchFn = requestOptions.dispatcher ? undiciFetch : globalThis.fetch; ``` ### Why not just always use undici's fetch? That would fix the crash - but it would break some tests. MSW (Mock Service Worker), which is used in our test suite to intercept HTTP requests, only hooks into `globalThis.fetch`. Undici's fetch bypasses those interceptors entirely, so tests would start making real network requests instead of getting the mocked responses. We could rewrite all tests to use undici-compatible mocking instead - but that would be a massive change for no real benefit. ---- Fixes #4093 --- js/http_fetcher.js | 11 ++++++--- tests/unit/functions/http_fetcher_spec.js | 29 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/js/http_fetcher.js b/js/http_fetcher.js index 830b08bd..07661684 100644 --- a/js/http_fetcher.js +++ b/js/http_fetcher.js @@ -1,5 +1,5 @@ const { EventEmitter } = require("node:events"); -const { Agent } = require("undici"); +const { fetch: undiciFetch, Agent } = require("undici"); const Log = require("logger"); const { getUserAgent } = require("#server_functions"); @@ -263,8 +263,13 @@ class HTTPFetcher extends EventEmitter { const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { - const response = await fetch(this.url, { - ...this.getRequestOptions(), + const requestOptions = this.getRequestOptions(); + // Use undici.fetch when a custom dispatcher is present (e.g. selfSignedCert), + // because Node's global fetch and npm undici@8 Agents are incompatible. + // For regular requests, use globalThis.fetch so MSW and other interceptors work. + const fetchFn = requestOptions.dispatcher ? undiciFetch : globalThis.fetch; + const response = await fetchFn(this.url, { + ...requestOptions, signal: controller.signal }); diff --git a/tests/unit/functions/http_fetcher_spec.js b/tests/unit/functions/http_fetcher_spec.js index 046ddb9c..0d8d41be 100644 --- a/tests/unit/functions/http_fetcher_spec.js +++ b/tests/unit/functions/http_fetcher_spec.js @@ -440,3 +440,32 @@ describe("fetch() method", () => { expect(errorInfo.errorType).toBe("NETWORK_ERROR"); }); }); + +describe("selfSignedCert dispatcher", () => { + const { Agent } = require("undici"); + + it("should set rejectUnauthorized=false when selfSignedCert is true", () => { + fetcher = new HTTPFetcher(TEST_URL, { + reloadInterval: 60000, + selfSignedCert: true + }); + + const options = fetcher.getRequestOptions(); + + expect(options.dispatcher).toBeInstanceOf(Agent); + const agentOptionsSymbol = Object.getOwnPropertySymbols(options.dispatcher).find((s) => s.description === "options"); + const dispatcherOptions = options.dispatcher[agentOptionsSymbol]; + expect(dispatcherOptions.connect.rejectUnauthorized).toBe(false); + }); + + it("should not set a dispatcher when selfSignedCert is false", () => { + fetcher = new HTTPFetcher(TEST_URL, { + reloadInterval: 60000, + selfSignedCert: false + }); + + const options = fetcher.getRequestOptions(); + + expect(options.dispatcher).toBeUndefined(); + }); +});