Skip to content

Commit 2e97e29

Browse files
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
1 parent d8c29d5 commit 2e97e29

File tree

2 files changed

+37
-3
lines changed

2 files changed

+37
-3
lines changed

js/http_fetcher.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { EventEmitter } = require("node:events");
2-
const { Agent } = require("undici");
2+
const { fetch: undiciFetch, Agent } = require("undici");
33
const Log = require("logger");
44
const { getUserAgent } = require("#server_functions");
55

@@ -263,8 +263,13 @@ class HTTPFetcher extends EventEmitter {
263263
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
264264

265265
try {
266-
const response = await fetch(this.url, {
267-
...this.getRequestOptions(),
266+
const requestOptions = this.getRequestOptions();
267+
// Use undici.fetch when a custom dispatcher is present (e.g. selfSignedCert),
268+
// because Node's global fetch and npm undici@8 Agents are incompatible.
269+
// For regular requests, use globalThis.fetch so MSW and other interceptors work.
270+
const fetchFn = requestOptions.dispatcher ? undiciFetch : globalThis.fetch;
271+
const response = await fetchFn(this.url, {
272+
...requestOptions,
268273
signal: controller.signal
269274
});
270275

tests/unit/functions/http_fetcher_spec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,32 @@ describe("fetch() method", () => {
440440
expect(errorInfo.errorType).toBe("NETWORK_ERROR");
441441
});
442442
});
443+
444+
describe("selfSignedCert dispatcher", () => {
445+
const { Agent } = require("undici");
446+
447+
it("should set rejectUnauthorized=false when selfSignedCert is true", () => {
448+
fetcher = new HTTPFetcher(TEST_URL, {
449+
reloadInterval: 60000,
450+
selfSignedCert: true
451+
});
452+
453+
const options = fetcher.getRequestOptions();
454+
455+
expect(options.dispatcher).toBeInstanceOf(Agent);
456+
const agentOptionsSymbol = Object.getOwnPropertySymbols(options.dispatcher).find((s) => s.description === "options");
457+
const dispatcherOptions = options.dispatcher[agentOptionsSymbol];
458+
expect(dispatcherOptions.connect.rejectUnauthorized).toBe(false);
459+
});
460+
461+
it("should not set a dispatcher when selfSignedCert is false", () => {
462+
fetcher = new HTTPFetcher(TEST_URL, {
463+
reloadInterval: 60000,
464+
selfSignedCert: false
465+
});
466+
467+
const options = fetcher.getRequestOptions();
468+
469+
expect(options.dispatcher).toBeUndefined();
470+
});
471+
});

0 commit comments

Comments
 (0)