From 6ae2bcc75078693bd7814b441fec59e407516913 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 6 Aug 2026 17:46:33 +0900 Subject: [PATCH 1/2] fix: add User-Agent header to OpenAI client fetch (#177) Some CDNs and WAFs (e.g. Cloudflare) block requests that arrive without a User-Agent. Inject a default one into the undici fetch wrapper, preserving any caller-provided headers. (cherry picked from commit 67a33160c870c301342eb6b3a70ae01609976c7e) --- packages/core/src/common/openai-client.ts | 17 ++++++++++++++- packages/core/src/tests/openai-client.test.ts | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/tests/openai-client.test.ts diff --git a/packages/core/src/common/openai-client.ts b/packages/core/src/common/openai-client.ts index d3b56c08..f80a780a 100644 --- a/packages/core/src/common/openai-client.ts +++ b/packages/core/src/common/openai-client.ts @@ -3,6 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; +import type { HeadersInit } from "undici"; import { resolveCurrentSettings } from "../settings"; // Custom undici Agent with a 180-second keepAlive timeout. The default @@ -12,6 +13,19 @@ import { resolveCurrentSettings } from "../settings"; // keep connections reusable for three minutes after the last request. const keepAliveAgent = new Agent({ keepAliveTimeout: 180_000 }); +const DEFAULT_USER_AGENT = `deepcode-cli (Node.js ${process.version})`; + +// Inject a default User-Agent header into outgoing requests. Some CDNs and +// WAFs (e.g. Cloudflare) block requests that arrive without a User-Agent as a +// basic anti-bot measure. (#177) +export function buildClientHeaders(initHeaders?: HeadersInit): Headers { + const headers = new Headers(initHeaders); + if (!headers.has("User-Agent")) { + headers.set("User-Agent", DEFAULT_USER_AGENT); + } + return headers; +} + // Module-level cache for the OpenAI client instance. The client itself is // a stateless fetch wrapper, so it is safe to share across calls as long as // the apiKey + baseURL stay the same. Model, thinking-mode and other @@ -73,7 +87,8 @@ export function createOpenAIClient(projectRoot: string = process.cwd()): { apiKey: settings.apiKey, baseURL: settings.baseURL || undefined, // eslint-disable-next-line @typescript-eslint/no-explicit-any - fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }), + fetch: (url: any, init: any) => + undiciFetch(url, { ...init, headers: buildClientHeaders(init?.headers), dispatcher: keepAliveAgent }), }); cachedOpenAIKey = cacheKey; diff --git a/packages/core/src/tests/openai-client.test.ts b/packages/core/src/tests/openai-client.test.ts new file mode 100644 index 00000000..9f9be693 --- /dev/null +++ b/packages/core/src/tests/openai-client.test.ts @@ -0,0 +1,21 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildClientHeaders } from "../common/openai-client"; + +test("buildClientHeaders injects a default User-Agent when none is set", () => { + const headers = buildClientHeaders(undefined); + assert.match(headers.get("User-Agent") ?? "", /^deepcode-cli \(Node\.js v\d+\.\d+\.\d+\)$/); +}); + +test("buildClientHeaders preserves existing headers and a custom User-Agent", () => { + const headers = buildClientHeaders({ Authorization: "Bearer sk-test", "User-Agent": "custom-agent" }); + assert.equal(headers.get("Authorization"), "Bearer sk-test"); + assert.equal(headers.get("User-Agent"), "custom-agent"); +}); + +test("buildClientHeaders accepts a Headers instance and keeps its entries", () => { + const source = new Headers({ "Content-Type": "application/json" }); + const headers = buildClientHeaders(source); + assert.equal(headers.get("Content-Type"), "application/json"); + assert.match(headers.get("User-Agent") ?? "", /^deepcode-cli /); +}); From b12ae48c2f0d120da0aaeb06bf02c10a757ed584 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 6 Aug 2026 18:20:53 +0900 Subject: [PATCH 2/2] test: use tuple-array HeadersInit in openai-client header test The undici HeadersInit type used by buildClientHeaders does not accept the global Headers instance in this project's TS config, so switch the test to a tuple-array input. (cherry picked from commit 8ee5241e83e602a87fd64151997dad06b79708f6) --- packages/core/src/tests/openai-client.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tests/openai-client.test.ts b/packages/core/src/tests/openai-client.test.ts index 9f9be693..6bb57d7a 100644 --- a/packages/core/src/tests/openai-client.test.ts +++ b/packages/core/src/tests/openai-client.test.ts @@ -13,9 +13,8 @@ test("buildClientHeaders preserves existing headers and a custom User-Agent", () assert.equal(headers.get("User-Agent"), "custom-agent"); }); -test("buildClientHeaders accepts a Headers instance and keeps its entries", () => { - const source = new Headers({ "Content-Type": "application/json" }); - const headers = buildClientHeaders(source); +test("buildClientHeaders accepts tuple-array headers and keeps its entries", () => { + const headers = buildClientHeaders([["Content-Type", "application/json"]]); assert.equal(headers.get("Content-Type"), "application/json"); assert.match(headers.get("User-Agent") ?? "", /^deepcode-cli /); });