Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
e794d93371 fix(fetch): respect dispatcher option from undici for TLS settings
When using undici's `Agent` class with `fetch()`, the TLS configuration
(such as `rejectUnauthorized: false`) was not being applied. This change:

1. Updates the stubbed undici `Agent` class to store constructor options
   and expose them via an `options` getter
2. Adds dispatcher option parsing in fetch to extract `connect.rejectUnauthorized`
   from the agent's options

The `tls` option still takes precedence over `dispatcher` settings when
both are provided.

Fixes #24376

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:09:37 +00:00
3 changed files with 174 additions and 1 deletions

View File

@@ -440,6 +440,58 @@ fn fetchImpl(
return .zero;
}
// "dispatcher: Dispatcher" - extract TLS options from undici-style dispatcher
// This allows compatibility with undici's Agent pattern:
// const agent = new Agent({ connect: { rejectUnauthorized: false } });
// fetch(url, { dispatcher: agent });
// NOTE: This is processed BEFORE the "tls" option so that explicit "tls"
// options take precedence over dispatcher settings.
extract_dispatcher: {
const objects_to_try = [_]JSValue{
options_object orelse .zero,
request_init_object orelse .zero,
};
inline for (0..2) |i| {
if (objects_to_try[i] != .zero) {
if (try objects_to_try[i].get(globalThis, "dispatcher")) |dispatcher| {
if (dispatcher.isObject()) {
// Get dispatcher.options (the Agent constructor options)
if (try dispatcher.get(globalThis, "options")) |dispatcher_options| {
if (dispatcher_options.isObject()) {
// Get dispatcher.options.connect
if (try dispatcher_options.get(globalThis, "connect")) |connect| {
if (connect.isObject()) {
// Get dispatcher.options.connect.rejectUnauthorized
if (try connect.get(globalThis, "rejectUnauthorized")) |reject| {
if (reject.isBoolean()) {
reject_unauthorized = reject.asBoolean();
} else if (reject.isNumber()) {
reject_unauthorized = reject.to(i32) != 0;
}
}
}
}
}
}
}
}
if (globalThis.hasException()) {
is_error = true;
return .zero;
}
}
}
break :extract_dispatcher;
}
if (globalThis.hasException()) {
is_error = true;
return .zero;
}
// "tls: TLSConfig"
ssl_config = extract_ssl_config: {
const objects_to_try = [_]JSValue{

View File

@@ -263,7 +263,18 @@ class MockAgent {
function mockErrors() {}
class Dispatcher extends EventEmitter {}
class Agent extends Dispatcher {}
class Agent extends Dispatcher {
#options;
constructor(options = {}) {
super();
this.#options = options;
}
get options() {
return this.#options;
}
}
class Pool extends Dispatcher {
request() {}
}

View File

@@ -0,0 +1,110 @@
import { describe, expect, it } from "bun:test";
import { expiredTls } from "harness";
import { Agent } from "undici";
// Test for issue #24376: fetch doesn't respect dispatcher option from undici
// The dispatcher option should allow setting TLS options like rejectUnauthorized
// via undici's Agent class.
describe("fetch with undici dispatcher", () => {
it("should respect rejectUnauthorized: false from dispatcher", async () => {
// Create a server with an expired/self-signed certificate
using server = Bun.serve({
port: 0,
tls: expiredTls,
fetch() {
return new Response("Hello World");
},
});
// Create an undici Agent with rejectUnauthorized: false
const agent = new Agent({
connect: {
rejectUnauthorized: false,
},
});
// This should succeed because the agent has rejectUnauthorized: false
const response = await fetch(`https://localhost:${server.port}`, {
dispatcher: agent,
});
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello World");
});
it("should fail with self-signed cert when dispatcher has rejectUnauthorized: true", async () => {
using server = Bun.serve({
port: 0,
tls: expiredTls,
fetch() {
return new Response("Hello World");
},
});
const agent = new Agent({
connect: {
rejectUnauthorized: true,
},
});
// This should fail because the certificate is invalid and rejectUnauthorized is true
expect(
fetch(`https://localhost:${server.port}`, {
dispatcher: agent,
}),
).rejects.toThrow();
});
it("should fail with self-signed cert when no dispatcher is provided", async () => {
using server = Bun.serve({
port: 0,
tls: expiredTls,
fetch() {
return new Response("Hello World");
},
});
// This should fail because the certificate is invalid and no TLS options are provided
expect(fetch(`https://localhost:${server.port}`)).rejects.toThrow();
});
it("tls option should take precedence over dispatcher", async () => {
using server = Bun.serve({
port: 0,
tls: expiredTls,
fetch() {
return new Response("Hello World");
},
});
// Agent says reject, but tls option says don't reject
const agent = new Agent({
connect: {
rejectUnauthorized: true,
},
});
// tls option should override dispatcher
const response = await fetch(`https://localhost:${server.port}`, {
dispatcher: agent,
tls: {
rejectUnauthorized: false,
},
});
expect(response.status).toBe(200);
});
it("Agent stores and exposes options", () => {
const options = {
connect: {
rejectUnauthorized: false,
timeout: 5000,
},
};
const agent = new Agent(options);
expect(agent.options).toEqual(options);
});
});