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
This commit is contained in:
Kristjan ESPERANTO
2026-04-08 18:42:30 +02:00
committed by GitHub
parent d8c29d5ec3
commit 2e97e29ab5
2 changed files with 37 additions and 3 deletions

View File

@@ -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
});

View File

@@ -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();
});
});