Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/core/src/code_assist/oauth2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { UserAccountManager } from '../utils/userAccountManager.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import http from 'node:http';
import * as https from 'node:https';
import open from 'open';
import crypto from 'node:crypto';
import * as os from 'node:os';
Expand Down Expand Up @@ -410,6 +411,78 @@ describe('oauth2', () => {
expect(mockOAuth2Client.setCredentials).toHaveBeenCalledWith(mockTokens);
});

describe('transporter keep-alive (nodejs/node#63989 workaround)', () => {
const setupUserCodeClient = () => {
const mockOAuth2Client = {
generateAuthUrl: vi.fn().mockReturnValue('https://example.com/auth'),
getToken: vi.fn().mockResolvedValue({
tokens: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
},
}),
generateCodeVerifierAsync: vi.fn().mockResolvedValue({
codeChallenge: 'test-challenge',
codeVerifier: 'test-verifier',
}),
getAccessToken: vi.fn().mockResolvedValue({ token: 'test-token' }),
setCredentials: vi.fn(),
on: vi.fn(),
credentials: {},
} as unknown as OAuth2Client;
vi.mocked(OAuth2Client).mockImplementation(() => mockOAuth2Client);
(readline.createInterface as Mock).mockReturnValue({
question: vi.fn((_query, callback) => callback('test-user-code')),
close: vi.fn(),
on: vi.fn(),
});
};

it('disables HTTP keep-alive when no proxy is configured', async () => {
setupUserCodeClient();
const config = {
getNoBrowser: () => true,
getProxy: () => undefined,
isBrowserLaunchSuppressed: () => true,
isInteractive: () => true,
} as unknown as Config;

await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);

const transporterOptions = (
vi.mocked(OAuth2Client).mock.calls[0]?.[0] as unknown as {
transporterOptions?: {
agent?: { keepAlive?: boolean };
proxy?: string;
};
}
)?.transporterOptions;
expect(transporterOptions?.agent).toBeInstanceOf(https.Agent);
expect(transporterOptions?.agent?.keepAlive).toBe(false);
expect(transporterOptions?.proxy).toBeUndefined();
});

it('keeps the proxy and no custom agent when a proxy is configured', async () => {
setupUserCodeClient();
const config = {
getNoBrowser: () => true,
getProxy: () => 'http://test.proxy.com:8080',
isBrowserLaunchSuppressed: () => true,
isInteractive: () => true,
} as unknown as Config;

await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);

const transporterOptions = (
vi.mocked(OAuth2Client).mock.calls[0]?.[0] as unknown as {
transporterOptions?: { agent?: unknown; proxy?: string };
}
)?.transporterOptions;
expect(transporterOptions?.proxy).toBe('http://test.proxy.com:8080');
expect(transporterOptions?.agent).toBeUndefined();
});
});

it('should cache Google Account when logging in with user code', async () => {
const mockConfigWithNoBrowser = {
getNoBrowser: () => true,
Expand Down
16 changes: 13 additions & 3 deletions packages/core/src/code_assist/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type JWTInput,
} from 'google-auth-library';
import * as http from 'node:http';
import * as https from 'node:https';
import url from 'node:url';
import crypto from 'node:crypto';
import * as net from 'node:net';
Expand Down Expand Up @@ -140,12 +141,21 @@ async function initOauthClient(
}
}

// gaxios/node-fetch reuse Node's global keep-alive agent for the token
// exchange. On Node >= 24.17.0 a regression in http.Agent socket reuse
// ("fix response queue poisoning") makes node-fetch throw a spurious
// ERR_STREAM_PREMATURE_CLOSE ("Premature close") when a pooled socket is
// reused, breaking OAuth sign-in. Disabling keep-alive forces a fresh
// connection per request and avoids the poisoned-socket race. When a proxy
// is configured we keep `proxy` instead, since passing our own agent would
// make gaxios ignore the proxy. See https://github.com/nodejs/node/issues/63989
const proxy = config.getProxy();
const client = new OAuth2Client({
clientId: OAUTH_CLIENT_ID,
clientSecret: OAUTH_CLIENT_SECRET,
transporterOptions: {
proxy: config.getProxy(),
},
transporterOptions: proxy
? { proxy }
: { agent: new https.Agent({ keepAlive: false }) },
});
const useEncryptedStorage = getUseEncryptedStorageFlag();

Expand Down