Skip to content

Commit 59b2dea

Browse files
authored
fix(cli): preserve Request headers in DevTools activity logger (#26078)
1 parent c841070 commit 59b2dea

2 files changed

Lines changed: 113 additions & 9 deletions

File tree

packages/cli/src/utils/activityLogger.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { describe, it, expect, beforeEach } from 'vitest';
7+
import { describe, it, expect, beforeEach, vi } from 'vitest';
88
import { ActivityLogger, type NetworkLog } from './activityLogger.js';
99
import type { ConsoleLogPayload } from '@google/gemini-cli-core';
1010

@@ -132,4 +132,95 @@ describe('ActivityLogger', () => {
132132
expect(after.console.length).toBe(0);
133133
expect(after.network.length).toBe(0);
134134
});
135+
136+
it('preserves headers and method from Request object when intercepting fetch', async () => {
137+
const originalFetch = global.fetch;
138+
139+
const mockFetch = vi.fn().mockImplementation(() =>
140+
Promise.resolve({
141+
status: 200,
142+
headers: new Headers(),
143+
body: null,
144+
clone: () => ({
145+
body: null,
146+
status: 200,
147+
headers: new Headers(),
148+
text: async () => 'ok',
149+
json: async () => ({}),
150+
}),
151+
} as unknown as Response),
152+
);
153+
154+
global.fetch = mockFetch;
155+
156+
try {
157+
// @ts-expect-error - accessing private property for testing
158+
logger.isInterceptionEnabled = false;
159+
logger.enable();
160+
161+
const request = new Request('https://api.example.com/data', {
162+
headers: { Authorization: 'Bearer test-token' },
163+
method: 'POST',
164+
});
165+
166+
await global.fetch(request);
167+
168+
expect(mockFetch).toHaveBeenCalled();
169+
const [, calledInit] = mockFetch.mock.calls[0];
170+
171+
expect(calledInit?.headers).toBeDefined();
172+
const headers = new Headers(calledInit?.headers as HeadersInit);
173+
expect(headers.get('Authorization')).toBe('Bearer test-token');
174+
expect(headers.has('x-activity-request-id')).toBe(true);
175+
expect(calledInit?.method).toBe('POST');
176+
} finally {
177+
global.fetch = originalFetch;
178+
// @ts-expect-error - reset private property
179+
logger.isInterceptionEnabled = false;
180+
}
181+
});
182+
183+
it('replaces Request headers with init headers (Fetch spec compliance)', async () => {
184+
const originalFetch = global.fetch;
185+
const mockFetch = vi.fn().mockImplementation(() =>
186+
Promise.resolve({
187+
status: 200,
188+
headers: new Headers(),
189+
body: null,
190+
clone: () => ({
191+
body: null,
192+
status: 200,
193+
headers: new Headers(),
194+
text: async () => 'ok',
195+
}),
196+
} as unknown as Response),
197+
);
198+
global.fetch = mockFetch;
199+
200+
try {
201+
// @ts-expect-error - accessing private property for testing
202+
logger.isInterceptionEnabled = false;
203+
logger.enable();
204+
205+
const request = new Request('https://api.example.com/data', {
206+
headers: { 'X-Old': 'old-value', 'X-Shared': 'old-shared' },
207+
});
208+
209+
await global.fetch(request, {
210+
headers: { 'X-New': 'new-value', 'X-Shared': 'new-shared' },
211+
});
212+
213+
const [, calledInit] = mockFetch.mock.calls[0];
214+
const headers = new Headers(calledInit?.headers as HeadersInit);
215+
216+
expect(headers.get('X-New')).toBe('new-value');
217+
expect(headers.get('X-Shared')).toBe('new-shared');
218+
expect(headers.has('X-Old')).toBe(false);
219+
expect(headers.has('x-activity-request-id')).toBe(true);
220+
} finally {
221+
global.fetch = originalFetch;
222+
// @ts-expect-error - reset private property
223+
logger.isInterceptionEnabled = false;
224+
}
225+
});
135226
});

packages/cli/src/utils/activityLogger.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -302,18 +302,31 @@ export class ActivityLogger extends EventEmitter {
302302
return originalFetch(input, init);
303303

304304
const id = Math.random().toString(36).substring(7);
305-
const method = (init?.method || 'GET').toUpperCase();
306305

307-
const newInit = { ...init };
308-
const headers = new Headers(init?.headers || {});
306+
const inputMethod =
307+
typeof input === 'object' && 'method' in input
308+
? input.method
309+
: undefined;
310+
const inputHeaders =
311+
typeof input === 'object' && 'headers' in input
312+
? input.headers
313+
: undefined;
314+
315+
const method = (init?.method ?? inputMethod ?? 'GET').toUpperCase();
316+
const headers = new Headers(init?.headers ?? inputHeaders ?? {});
309317
headers.set(ACTIVITY_ID_HEADER, id);
310-
newInit.headers = headers;
318+
319+
const newInit = {
320+
...init,
321+
method,
322+
headers,
323+
};
311324

312325
let reqBody = '';
313-
if (init?.body) {
314-
if (typeof init.body === 'string') reqBody = init.body;
315-
else if (init.body instanceof URLSearchParams)
316-
reqBody = init.body.toString();
326+
const body = newInit.body;
327+
if (body) {
328+
if (typeof body === 'string') reqBody = body;
329+
else if (body instanceof URLSearchParams) reqBody = body.toString();
317330
}
318331

319332
this.requestStartTimes.set(id, Date.now());

0 commit comments

Comments
 (0)