diff --git a/client/src/lib/hooks/__tests__/useConnection.test.tsx b/client/src/lib/hooks/__tests__/useConnection.test.tsx index 81bc4f45..4946cdda 100644 --- a/client/src/lib/hooks/__tests__/useConnection.test.tsx +++ b/client/src/lib/hooks/__tests__/useConnection.test.tsx @@ -913,6 +913,35 @@ describe("useConnection", () => { expect(headers).toHaveProperty("Authorization", "Bearer mock-token"); }); + test("replaces empty Bearer token placeholder with OAuth token", async () => { + // This test prevents regression of the bug where default "Bearer " header + // prevented OAuth token injection, causing infinite auth loops + const customHeaders: CustomHeaders = [ + { + name: "Authorization", + value: "Bearer ", // Empty Bearer token placeholder + enabled: true, + }, + ]; + + const propsWithEmptyBearer = { + ...defaultProps, + customHeaders, + }; + + const { result } = renderHook(() => useConnection(propsWithEmptyBearer)); + + await act(async () => { + await result.current.connect(); + }); + + const headers = mockSSETransport.options?.requestInit?.headers; + // Should replace the empty "Bearer " with actual OAuth token + expect(headers).toHaveProperty("Authorization", "Bearer mock-token"); + // Should not have the x-custom-auth-headers since Authorization is standard + expect(headers).not.toHaveProperty("x-custom-auth-headers"); + }); + test("prioritizes custom headers over legacy auth", async () => { const customHeaders: CustomHeaders = [ { name: "Authorization", value: "Bearer custom-token", enabled: true }, diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts index 163231eb..70ae5acf 100644 --- a/client/src/lib/hooks/useConnection.ts +++ b/client/src/lib/hooks/useConnection.ts @@ -400,11 +400,25 @@ export function useConnection({ // Use custom headers (migration is handled in App.tsx) let finalHeaders: CustomHeaders = customHeaders || []; - // Add OAuth token if available and no custom headers are set - if (finalHeaders.length === 0) { + // Check if we need to inject OAuth token + // This handles both empty headers and default "Bearer " placeholder headers + const isEmptyAuthHeader = (header: CustomHeaders[number]) => + header.name.trim().toLowerCase() === "authorization" && + header.value.trim() === "Bearer"; + + const needsOAuthToken = + finalHeaders.length === 0 || + finalHeaders.some( + (header) => header.enabled && isEmptyAuthHeader(header), + ); + + if (needsOAuthToken) { const oauthToken = (await serverAuthProvider.tokens())?.access_token; if (oauthToken) { + // Add the OAuth token finalHeaders = [ + // Remove any existing Authorization headers with empty tokens + ...finalHeaders.filter((header) => !isEmptyAuthHeader(header)), { name: "Authorization", value: `Bearer ${oauthToken}`,