Skip to content

Commit d0a1014

Browse files
ihor-sokoliukclaude
andcommitted
fix(proxy): merge User-Agent via Headers so applySearchRequestConfig preserves any HeadersInit shape (BUG-009)
Addresses Copilot review on PR #145: the object-spread merge could silently drop existing headers if a caller passed a Headers instance or tuple-array HeadersInit. Normalize through a Headers instance and write the result back as a plain object, so already-set headers are preserved regardless of shape. Both current callers pass no prior headers, so behavior is unchanged; the four UA assertions now read the normalized lowercase `user-agent` key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5c3958a commit d0a1014

3 files changed

Lines changed: 14 additions & 12 deletions

File tree

__tests__/unit/instance-info.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ async function runTests() {
455455
await fetchInstanceInfo(mockServer as any);
456456

457457
const headers = getCapturedOptions()?.headers as Record<string, string>;
458-
assert.equal(headers?.['User-Agent'], 'MyBot/1.0');
458+
assert.equal(headers?.['user-agent'], 'MyBot/1.0');
459459

460460
fetchMocker.restore();
461461
envManager.restore();
@@ -475,7 +475,7 @@ async function runTests() {
475475
await fetchInstanceInfo(mockServer as any);
476476

477477
const headers = (getCapturedOptions()?.headers || {}) as Record<string, string>;
478-
assert.ok(!headers['User-Agent'], `Expected no User-Agent header`);
478+
assert.ok(!headers['user-agent'], `Expected no User-Agent header`);
479479

480480
fetchMocker.restore();
481481
envManager.restore();

__tests__/unit/suggestions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ async function runTests() {
184184
await performSearchSuggestions(mockServer as any, 'type');
185185

186186
const headers = getCapturedOptions()?.headers as Record<string, string>;
187-
assert.equal(headers?.['User-Agent'], 'MyBot/1.0');
187+
assert.equal(headers?.['user-agent'], 'MyBot/1.0');
188188

189189
fetchMocker.restore();
190190
envManager.restore();
@@ -204,7 +204,7 @@ async function runTests() {
204204
await performSearchSuggestions(mockServer as any, 'type');
205205

206206
const headers = (getCapturedOptions()?.headers || {}) as Record<string, string>;
207-
assert.ok(!headers['User-Agent'], `Expected no User-Agent header`);
207+
assert.ok(!headers['user-agent'], `Expected no User-Agent header`);
208208

209209
fetchMocker.restore();
210210
envManager.restore();

src/proxy.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,10 @@ export function createDefaultAgent(): Agent | undefined {
297297
* still builds its own options in `search.ts` (Basic-Auth handling is interleaved
298298
* there); folding it in here is tracked under FEAT-050.
299299
*
300-
* Callers pass a freshly-built `RequestInit`; `headers`, when already present, is a
301-
* plain object (the convention across this codebase), so the spread-merge preserves
302-
* any existing entries — other `HeadersInit` shapes (a `Headers` instance or tuple
303-
* array) are never passed here and are intentionally not normalized.
300+
* The User-Agent is merged through a `Headers` instance, so any already-set
301+
* `headers` — whether a plain object, a `Headers` instance, or a tuple array —
302+
* is preserved; the result is written back as a plain object. (In practice both
303+
* callers pass a freshly-built `RequestInit` with no prior headers.)
304304
*/
305305
export function applySearchRequestConfig(
306306
requestOptions: RequestInit,
@@ -314,10 +314,12 @@ export function applySearchRequestConfig(
314314

315315
const userAgent = process.env.USER_AGENT;
316316
if (userAgent) {
317-
requestOptions.headers = {
318-
...requestOptions.headers,
319-
"User-Agent": userAgent,
320-
};
317+
// Normalize via Headers so any HeadersInit shape (plain object, Headers
318+
// instance, or tuple array) merges without dropping already-set entries,
319+
// then hand back a plain object.
320+
const headers = new Headers(requestOptions.headers);
321+
headers.set("User-Agent", userAgent);
322+
requestOptions.headers = Object.fromEntries(headers);
321323
}
322324
}
323325

0 commit comments

Comments
 (0)