diff --git a/packages/@n8n/ai-utilities/src/__tests__/utils/http-proxy-agent.test.ts b/packages/@n8n/ai-utilities/src/__tests__/utils/http-proxy-agent.test.ts index 98b2d7db149..9728497be6a 100644 --- a/packages/@n8n/ai-utilities/src/__tests__/utils/http-proxy-agent.test.ts +++ b/packages/@n8n/ai-utilities/src/__tests__/utils/http-proxy-agent.test.ts @@ -1,4 +1,4 @@ -import { Agent, ProxyAgent } from 'undici'; +import { Agent, ProxyAgent, fetch as undiciFetch } from 'undici'; import type { MockedFunction } from 'vitest'; import { getNodeProxyAgent, getProxyAgent, proxyFetch } from 'src/utils/http-proxy-agent'; @@ -11,11 +11,9 @@ vi.mock('undici', () => ({ ProxyAgent: vi.fn(function (options) { return { type: 'ProxyAgent', options }; }), + fetch: vi.fn(), })); -// Mock global fetch -global.fetch = vi.fn(); - describe('getProxyAgent', () => { // Store original environment variables const originalEnv = { ...process.env }; @@ -308,7 +306,7 @@ describe('getProxyAgent', () => { describe('proxyFetch', () => { // Store original environment variables const originalEnv = { ...process.env }; - const mockFetch = global.fetch as MockedFunction; + const mockFetch = undiciFetch as unknown as MockedFunction; // Reset environment variables and mocks before each test beforeEach(() => { diff --git a/packages/@n8n/ai-utilities/src/utils/http-proxy-agent.ts b/packages/@n8n/ai-utilities/src/utils/http-proxy-agent.ts index 1f7903e0d13..320dbfb2894 100644 --- a/packages/@n8n/ai-utilities/src/utils/http-proxy-agent.ts +++ b/packages/@n8n/ai-utilities/src/utils/http-proxy-agent.ts @@ -21,7 +21,7 @@ import { createHttpsProxyAgent, resolveProxyUrl } from '@n8n/backend-network/pro import type { AgentOptions } from 'node:https'; import type { LookupFunction } from 'node:net'; /* eslint-disable n8n-local-rules/no-uncentralized-http -- langchain consumers pin undici v6, incompatible with backend-network's v7 dispatchers; see block comment below */ -import { Agent, ProxyAgent } from 'undici'; +import { Agent, ProxyAgent, fetch as undiciFetch } from 'undici'; /** * Options for configuring HTTP agent timeouts. @@ -113,11 +113,12 @@ export async function proxyFetch( const targetUrl = input instanceof Request ? input.url : input.toString(); const dispatcher = getProxyAgent(targetUrl, timeoutOptions, lookup); - return await fetch(input, { - ...init, - // @ts-expect-error - dispatcher is an undici-specific option not in standard fetch + // The dispatcher comes from this package's undici, so the request must use + // the same undici's fetch: the global fetch on Node >= 26 rejects it. + return (await undiciFetch(input as Parameters[0], { + ...(init as Parameters[1]), dispatcher, - }); + })) as unknown as Response; } /** diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts index ee65ff0828d..d18add52e2f 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts @@ -1,8 +1,13 @@ +import { proxyFetch } from '@n8n/ai-utilities'; import type { ILoadOptionsFunctions } from 'n8n-workflow'; import type { Mocked } from 'vitest'; import { searchModels } from '../searchModels'; +vi.mock('@n8n/ai-utilities', () => ({ + proxyFetch: vi.fn(), +})); + const mockModels = [ { id: 'claude-3-opus-20240229', @@ -51,11 +56,12 @@ describe('searchModels', () => { json: async () => ({ data: mockModels }), text: async () => '', }); - vi.stubGlobal('fetch', fetchSpy); + vi.mocked(proxyFetch).mockImplementation( + fetchSpy as unknown as typeof import('@n8n/ai-utilities')['proxyFetch'], + ); }); afterEach(() => { - vi.unstubAllGlobals(); vi.clearAllMocks(); }); diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts index 595410725b9..3504432bef7 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts @@ -1,4 +1,4 @@ -import { getProxyAgent } from '@n8n/ai-utilities'; +import { proxyFetch } from '@n8n/ai-utilities'; import { listAnthropicModels } from '@n8n/ai-utilities/model-discovery'; import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow'; @@ -19,11 +19,7 @@ export async function searchModels( apiKey: (credentials.apiKey as string) ?? '', baseURL, headers: mergeCustomHeaders(credentials, {}), - fetch: async (url, init) => - await fetch(url, { - ...init, - dispatcher: getProxyAgent(baseURL), - } as RequestInit), + fetch: proxyFetch, }); return { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/__tests__/loadModels.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/__tests__/loadModels.test.ts index b507fbfc7d4..e7b2782b1d4 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/__tests__/loadModels.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/__tests__/loadModels.test.ts @@ -1,8 +1,13 @@ +import { proxyFetch } from '@n8n/ai-utilities'; import type { ILoadOptionsFunctions } from 'n8n-workflow'; import type { Mocked } from 'vitest'; import { searchModels } from '../loadModels'; +vi.mock('@n8n/ai-utilities', () => ({ + proxyFetch: vi.fn(), +})); + const MODEL_IDS = [ 'gpt-4', 'gpt-3.5-turbo', @@ -43,7 +48,9 @@ describe('searchModels', () => { json: async () => ({ data: MODEL_IDS.map((id) => ({ id })) }), text: async () => '', }); - vi.stubGlobal('fetch', fetchSpy); + vi.mocked(proxyFetch).mockImplementation( + fetchSpy as unknown as typeof import('@n8n/ai-utilities')['proxyFetch'], + ); }); afterEach(() => { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/loadModels.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/loadModels.ts index dba8c158693..2fd07949d6d 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/loadModels.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/methods/loadModels.ts @@ -1,4 +1,4 @@ -import { getProxyAgent } from '@n8n/ai-utilities'; +import { proxyFetch } from '@n8n/ai-utilities'; import { listOpenAiModels } from '@n8n/ai-utilities/model-discovery'; import { AiConfig } from '@n8n/config'; import { Container } from '@n8n/di'; @@ -24,11 +24,7 @@ export async function searchModels( apiKey: credentials.apiKey as string, baseURL, headers, - fetch: async (url, init) => - await fetch(url, { - ...init, - dispatcher: getProxyAgent(baseURL), - } as RequestInit), + fetch: proxyFetch, }); return { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/test/LmChatAnthropic.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/test/LmChatAnthropic.test.ts index cc41e73151c..d05d9c0235d 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/test/LmChatAnthropic.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/test/LmChatAnthropic.test.ts @@ -1,7 +1,12 @@ /* eslint-disable n8n-nodes-base/node-filename-against-convention */ /* eslint-disable @typescript-eslint/unbound-method */ import { ChatAnthropic } from '@langchain/anthropic'; -import { N8nLlmTracing, makeN8nLlmFailedAttemptHandler, getProxyAgent } from '@n8n/ai-utilities'; +import { + N8nLlmTracing, + makeN8nLlmFailedAttemptHandler, + getProxyAgent, + proxyFetch, +} from '@n8n/ai-utilities'; import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers'; import type { ILoadOptionsFunctions, INode, ISupplyDataFunctions } from 'n8n-workflow'; import type { Mock, Mocked } from 'vitest'; @@ -425,7 +430,9 @@ describe('LmChatAnthropic', () => { beforeEach(() => { mockGetCredentials = vi.fn(); fetchSpy = vi.fn(); - vi.stubGlobal('fetch', fetchSpy); + vi.mocked(proxyFetch).mockImplementation( + fetchSpy as unknown as typeof import('@n8n/ai-utilities')['proxyFetch'], + ); mockLoadContext = { getCredentials: mockGetCredentials,