diff --git a/core/continueServer/stubs/client.ts b/core/continueServer/stubs/client.ts index febed8bbd48..b1ef66ed515 100644 --- a/core/continueServer/stubs/client.ts +++ b/core/continueServer/stubs/client.ts @@ -1,3 +1,6 @@ +import { fetchwithRequestOptions } from "@continuedev/fetch"; + +import type { RequestOptions } from "../../index.js"; import type { ArtifactType, EmbeddingsCacheResponse, @@ -10,6 +13,7 @@ export class ContinueServerClient implements IContinueServerClient { constructor( serverUrl: string | undefined, private readonly userToken: string | undefined, + private readonly requestOptions?: RequestOptions, ) { try { this.url = @@ -32,18 +36,22 @@ export class ContinueServerClient implements IContinueServerClient { public async getConfig(): Promise<{ configJson: string }> { const userToken = await this.userToken; - const response = await fetch(new URL("sync", this.url).href, { - method: "GET", - headers: { - Authorization: `Bearer ${userToken}`, + const response = await fetchwithRequestOptions( + new URL("sync", this.url).href, + { + method: "GET", + headers: { + Authorization: `Bearer ${userToken}`, + }, }, - }); + this.requestOptions, + ); if (!response.ok) { throw new Error( `Failed to sync remote config (HTTP ${response.status}): ${response.statusText}`, ); } - const data = await response.json(); + const data = (await response.json()) as { configJson: string }; return data; } @@ -66,17 +74,21 @@ export class ContinueServerClient implements IContinueServerClient { const url = new URL("indexing/cache", this.url); try { - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${await this.userToken}`, + const response = await fetchwithRequestOptions( + url, + { + method: "POST", + headers: { + Authorization: `Bearer ${await this.userToken}`, + }, + body: JSON.stringify({ + keys, + artifactId, + repo: repoName ?? "NONE", + }), }, - body: JSON.stringify({ - keys, - artifactId, - repo: repoName ?? "NONE", - }), - }); + this.requestOptions, + ); if (!response.ok) { const text = await response.text(); @@ -88,7 +100,7 @@ export class ContinueServerClient implements IContinueServerClient { }; } - const data = await response.json(); + const data = (await response.json()) as EmbeddingsCacheResponse; return data; } catch (e) { console.warn("Failed to retrieve from remote cache", e); @@ -105,15 +117,19 @@ export class ContinueServerClient implements IContinueServerClient { const url = new URL("feedback", this.url); - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${await this.userToken}`, + const response = await fetchwithRequestOptions( + url, + { + method: "POST", + headers: { + Authorization: `Bearer ${await this.userToken}`, + }, + body: JSON.stringify({ + feedback, + data, + }), }, - body: JSON.stringify({ - feedback, - data, - }), - }); + this.requestOptions, + ); } } diff --git a/core/indexing/CodebaseIndexer.ts b/core/indexing/CodebaseIndexer.ts index c37992f98cd..8abab6c3d87 100644 --- a/core/indexing/CodebaseIndexer.ts +++ b/core/indexing/CodebaseIndexer.ts @@ -161,6 +161,7 @@ export class CodebaseIndexer { const continueServerClient = new ContinueServerClient( ideSettings.remoteConfigServerUrl, ideSettings.userToken, + config.requestOptions, ); if (!continueServerClient) { return []; diff --git a/packages/fetch/src/fetch.test.ts b/packages/fetch/src/fetch.test.ts new file mode 100644 index 00000000000..1bf5fee8769 --- /dev/null +++ b/packages/fetch/src/fetch.test.ts @@ -0,0 +1,66 @@ +import { HttpsProxyAgent } from "https-proxy-agent"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { fetchwithRequestOptions } from "./fetch.js"; +import patchedFetch from "./node-fetch-patch.js"; + +vi.mock("./node-fetch-patch.js", () => ({ + default: vi.fn(), +})); + +const originalEnv = process.env; + +beforeEach(() => { + process.env = { ...originalEnv }; + delete process.env.HTTP_PROXY; + delete process.env.HTTPS_PROXY; + delete process.env.http_proxy; + delete process.env.https_proxy; + + vi.mocked(patchedFetch).mockReset(); + vi.mocked(patchedFetch).mockResolvedValue({ + ok: true, + headers: { get: () => undefined }, + } as any); +}); + +afterEach(() => { + process.env = originalEnv; +}); + +test("fetchwithRequestOptions uses an HttpsProxyAgent configured with requestOptions.proxy", async () => { + await fetchwithRequestOptions("https://example.com/api", undefined, { + proxy: "http://proxy.example.com:8080", + }); + + expect(patchedFetch).toHaveBeenCalledTimes(1); + const [, init] = vi.mocked(patchedFetch).mock.calls[0]; + const agent = (init as any).agent; + + expect(agent).toBeInstanceOf(HttpsProxyAgent); + expect(agent.proxy.href).toBe("http://proxy.example.com:8080/"); +}); + +test("fetchwithRequestOptions disables TLS verification when requestOptions.verifySsl is false", async () => { + await fetchwithRequestOptions("https://example.com/api", undefined, { + verifySsl: false, + }); + + const [, init] = vi.mocked(patchedFetch).mock.calls[0]; + const agent = (init as any).agent; + + expect(agent.options.rejectUnauthorized).toBe(false); +}); + +test("fetchwithRequestOptions honors proxy and disabled TLS verification together", async () => { + await fetchwithRequestOptions("https://example.com/api", undefined, { + proxy: "http://proxy.example.com:8080", + verifySsl: false, + }); + + const [, init] = vi.mocked(patchedFetch).mock.calls[0]; + const agent = (init as any).agent; + + expect(agent).toBeInstanceOf(HttpsProxyAgent); + expect(agent.proxy.href).toBe("http://proxy.example.com:8080/"); + expect(agent.connectOpts.rejectUnauthorized).toBe(false); +});