diff --git a/packages/cli/src/utils/activityLogger.test.ts b/packages/cli/src/utils/activityLogger.test.ts index e80eff6485..9d73c0d19f 100644 --- a/packages/cli/src/utils/activityLogger.test.ts +++ b/packages/cli/src/utils/activityLogger.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ActivityLogger, type NetworkLog } from './activityLogger.js'; import type { ConsoleLogPayload } from '@google/gemini-cli-core'; @@ -132,4 +132,95 @@ describe('ActivityLogger', () => { expect(after.console.length).toBe(0); expect(after.network.length).toBe(0); }); + + it('preserves headers and method from Request object when intercepting fetch', async () => { + const originalFetch = global.fetch; + + const mockFetch = vi.fn().mockImplementation(() => + Promise.resolve({ + status: 200, + headers: new Headers(), + body: null, + clone: () => ({ + body: null, + status: 200, + headers: new Headers(), + text: async () => 'ok', + json: async () => ({}), + }), + } as unknown as Response), + ); + + global.fetch = mockFetch; + + try { + // @ts-expect-error - accessing private property for testing + logger.isInterceptionEnabled = false; + logger.enable(); + + const request = new Request('https://api.example.com/data', { + headers: { Authorization: 'Bearer test-token' }, + method: 'POST', + }); + + await global.fetch(request); + + expect(mockFetch).toHaveBeenCalled(); + const [, calledInit] = mockFetch.mock.calls[0]; + + expect(calledInit?.headers).toBeDefined(); + const headers = new Headers(calledInit?.headers as HeadersInit); + expect(headers.get('Authorization')).toBe('Bearer test-token'); + expect(headers.has('x-activity-request-id')).toBe(true); + expect(calledInit?.method).toBe('POST'); + } finally { + global.fetch = originalFetch; + // @ts-expect-error - reset private property + logger.isInterceptionEnabled = false; + } + }); + + it('replaces Request headers with init headers (Fetch spec compliance)', async () => { + const originalFetch = global.fetch; + const mockFetch = vi.fn().mockImplementation(() => + Promise.resolve({ + status: 200, + headers: new Headers(), + body: null, + clone: () => ({ + body: null, + status: 200, + headers: new Headers(), + text: async () => 'ok', + }), + } as unknown as Response), + ); + global.fetch = mockFetch; + + try { + // @ts-expect-error - accessing private property for testing + logger.isInterceptionEnabled = false; + logger.enable(); + + const request = new Request('https://api.example.com/data', { + headers: { 'X-Old': 'old-value', 'X-Shared': 'old-shared' }, + }); + + await global.fetch(request, { + headers: { 'X-New': 'new-value', 'X-Shared': 'new-shared' }, + }); + + const [, calledInit] = mockFetch.mock.calls[0]; + const headers = new Headers(calledInit?.headers as HeadersInit); + + expect(headers.get('X-New')).toBe('new-value'); + expect(headers.get('X-Shared')).toBe('new-shared'); + expect(headers.has('X-Old')).toBe(false); + expect(headers.has('x-activity-request-id')).toBe(true); + } finally { + global.fetch = originalFetch; + // @ts-expect-error - reset private property + logger.isInterceptionEnabled = false; + } + }); }); diff --git a/packages/cli/src/utils/activityLogger.ts b/packages/cli/src/utils/activityLogger.ts index 8118ccdde9..8f47b39808 100644 --- a/packages/cli/src/utils/activityLogger.ts +++ b/packages/cli/src/utils/activityLogger.ts @@ -302,18 +302,31 @@ export class ActivityLogger extends EventEmitter { return originalFetch(input, init); const id = Math.random().toString(36).substring(7); - const method = (init?.method || 'GET').toUpperCase(); - const newInit = { ...init }; - const headers = new Headers(init?.headers || {}); + const inputMethod = + typeof input === 'object' && 'method' in input + ? input.method + : undefined; + const inputHeaders = + typeof input === 'object' && 'headers' in input + ? input.headers + : undefined; + + const method = (init?.method ?? inputMethod ?? 'GET').toUpperCase(); + const headers = new Headers(init?.headers ?? inputHeaders ?? {}); headers.set(ACTIVITY_ID_HEADER, id); - newInit.headers = headers; + + const newInit = { + ...init, + method, + headers, + }; let reqBody = ''; - if (init?.body) { - if (typeof init.body === 'string') reqBody = init.body; - else if (init.body instanceof URLSearchParams) - reqBody = init.body.toString(); + const body = newInit.body; + if (body) { + if (typeof body === 'string') reqBody = body; + else if (body instanceof URLSearchParams) reqBody = body.toString(); } this.requestStartTimes.set(id, Date.now());