diff --git a/src/backend/ai/providers/http.ts b/src/backend/ai/providers/http.ts index 6195da86..b3f6b4eb 100644 --- a/src/backend/ai/providers/http.ts +++ b/src/backend/ai/providers/http.ts @@ -1,4 +1,4 @@ -import { getFetchDispatcher } from "../../utils/proxy-agent.js"; +import { fetchWithProxy } from "../../utils/proxy-agent.js"; import { safeOutboundFetch } from "../../utils/safe-outbound-fetch.js"; import { evaluateEgress, readPrivateAllowlist } from "../egress.js"; import { AiProviderError } from "./types.js"; @@ -9,8 +9,8 @@ import { AiProviderError } from "./types.js"; * * Public hosts use safeOutboundFetch, which re-checks the resolved address at * connect time. Allowlisted private hosts cannot use it (its whole job is to - * refuse them), so they fall back to plain fetch with the proxy dispatcher -- - * still respecting corporate proxy configuration. + * refuse them), so they use the installed Undici fetch implementation with + * its matching proxy dispatcher -- still respecting proxy configuration. */ export async function providerFetch( url: string, @@ -24,10 +24,7 @@ export async function providerFetch( } if (decision.isPrivate) { - return fetch(url, { - ...init, - dispatcher: getFetchDispatcher(url), - } as RequestInit); + return fetchWithProxy(url, init); } return safeOutboundFetch(url, init) as unknown as Promise; diff --git a/src/backend/tests/ai/provider-http.test.ts b/src/backend/tests/ai/provider-http.test.ts new file mode 100644 index 00000000..928b6261 --- /dev/null +++ b/src/backend/tests/ai/provider-http.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fetchWithProxy = vi.fn(); +const getFetchDispatcher = vi.fn(); +const safeOutboundFetch = vi.fn(); +const readPrivateAllowlist = vi.fn(); +const evaluateEgress = vi.fn(); +const globalFetch = vi.fn(); + +vi.mock("../../utils/proxy-agent.js", () => ({ + fetchWithProxy, + getFetchDispatcher, +})); +vi.mock("../../utils/safe-outbound-fetch.js", () => ({ safeOutboundFetch })); +vi.mock("../../ai/egress.js", () => ({ + readPrivateAllowlist, + evaluateEgress, +})); + +const { providerFetch } = await import("../../ai/providers/http.js"); + +describe("providerFetch", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", globalFetch); + readPrivateAllowlist.mockResolvedValue(["192.168.1.50"]); + }); + + it("uses the matching Undici fetch implementation for private providers", async () => { + const response = { ok: true, status: 200 } as Response; + const init = { method: "GET" }; + evaluateEgress.mockReturnValue({ allowed: true, isPrivate: true }); + fetchWithProxy.mockResolvedValue(response); + + await expect( + providerFetch("http://192.168.1.50:11434/api/tags", init), + ).resolves.toBe(response); + + expect(fetchWithProxy).toHaveBeenCalledWith( + "http://192.168.1.50:11434/api/tags", + init, + ); + expect(globalFetch).not.toHaveBeenCalled(); + expect(safeOutboundFetch).not.toHaveBeenCalled(); + }); +});