Skip to content

Commit 702b1c0

Browse files
sarahxsandersclaude
authored andcommitted
fix(api): widen dual-stack fallback process-wide
Move the IPv6-to-IPv4 fallback timeout from per-request https agents to a single process-wide setting. The per-request agents only covered four axios calls, so `detectRegion` in src/utils/urls.ts still failed first, along with the oauth, provisioning and MCP profile calls. Setting the Node default covers every client in the process, including posthog-node, the agent SDK and MCP. Replaces the two mock-based tests, which asserted the config object was passed rather than that the timeout changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c6335fb commit 702b1c0

7 files changed

Lines changed: 51 additions & 81 deletions

File tree

bin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { satisfies } from 'semver';
33
import { ErrorCodes } from './src/lib/errors/codes.js';
44
import { emitWizardError } from './src/lib/errors/emit.js';
5+
// Only pulls in node:net, so safe to hoist above the version guard.
6+
import { configureDualStackFallback } from './src/lib/net-tuning.js';
57

68
// Keep in sync with `engines.node` in package.json. npx does not enforce
79
// engines, so this preflight is the only thing standing between an old Node
@@ -35,6 +37,9 @@ if (!satisfies(process.version, NODE_VERSION_RANGE)) {
3537
process.exit(1);
3638
}
3739

40+
// Must run before any network client opens a connection.
41+
configureDualStackFallback();
42+
3843
// Test mock server — only loaded when NODE_ENV is 'test'.
3944
// In production builds, tsdown replaces process.env.NODE_ENV with 'production',
4045
// making this block dead code.

src/lib/__tests__/api-transport.test.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import {
3+
DUAL_STACK_ATTEMPT_TIMEOUT_MS,
4+
configureDualStackFallback,
5+
dualStackNet,
6+
} from '../net-tuning';
7+
8+
describe('configureDualStackFallback', () => {
9+
const original = dualStackNet.getDefaultAutoSelectFamilyAttemptTimeout();
10+
11+
afterEach(() => {
12+
dualStackNet.setDefaultAutoSelectFamilyAttemptTimeout(original);
13+
});
14+
15+
it('raises the attempt timeout above the 250ms Node default', () => {
16+
configureDualStackFallback();
17+
18+
expect(dualStackNet.getDefaultAutoSelectFamilyAttemptTimeout()).toBe(
19+
DUAL_STACK_ATTEMPT_TIMEOUT_MS,
20+
);
21+
});
22+
23+
it('leaves enough headroom for a slow IPv4 connect', () => {
24+
expect(DUAL_STACK_ATTEMPT_TIMEOUT_MS).toBeGreaterThanOrEqual(1_000);
25+
});
26+
});

src/lib/api.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import { Agent } from 'node:https';
21
import axios, { AxiosError } from 'axios';
32
import { z } from 'zod';
43
import { analytics } from '@utils/analytics';
54
import { WIZARD_USER_AGENT } from './constants';
65

7-
// Node defaults to 250ms between IPv6 and IPv4 connection attempts, which is
8-
// too short for some regional PostHog API connections after OAuth completes.
9-
export const posthogApiHttpsAgent = new Agent({
10-
autoSelectFamilyAttemptTimeout: 2_000,
11-
});
12-
136
/**
147
* User payload from `/api/users/@me/`. Schema typed for the fields the
158
* wizard actually reads + passthrough on everything else so the full
@@ -155,7 +148,6 @@ export async function fetchUserData(
155148
Authorization: `Bearer ${accessToken}`,
156149
'User-Agent': WIZARD_USER_AGENT,
157150
},
158-
httpsAgent: posthogApiHttpsAgent,
159151
});
160152

161153
return ApiUserSchema.parse(response.data);
@@ -195,7 +187,6 @@ export async function fetchRecentActivity(
195187
Authorization: `Bearer ${accessToken}`,
196188
'User-Agent': WIZARD_USER_AGENT,
197189
},
198-
httpsAgent: posthogApiHttpsAgent,
199190
// Short timeout — best-effort probe, not a critical path.
200191
timeout: 4000,
201192
},
@@ -224,7 +215,6 @@ export async function fetchProjectData(
224215
Authorization: `Bearer ${accessToken}`,
225216
'User-Agent': WIZARD_USER_AGENT,
226217
},
227-
httpsAgent: posthogApiHttpsAgent,
228218
});
229219

230220
return ApiProjectSchema.parse(response.data);
@@ -263,7 +253,6 @@ export async function fetchSlackConnected(
263253
Authorization: `Bearer ${accessToken}`,
264254
'User-Agent': WIZARD_USER_AGENT,
265255
},
266-
httpsAgent: posthogApiHttpsAgent,
267256
signal,
268257
},
269258
);

src/lib/net-tuning.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import net from 'node:net';
2+
3+
/** Node's 250ms default kills a slow IPv4 connect when IPv6 is unroutable. */
4+
export const DUAL_STACK_ATTEMPT_TIMEOUT_MS = 2_000;
5+
6+
// @types/node is pinned to v18; these are Node 20+ APIs.
7+
type DualStackNet = {
8+
setDefaultAutoSelectFamilyAttemptTimeout(ms: number): void;
9+
getDefaultAutoSelectFamilyAttemptTimeout(): number;
10+
};
11+
12+
export const dualStackNet = net as unknown as DualStackNet;
13+
14+
/** Process-wide, so axios, fetch, posthog-node, the agent SDK and MCP all get it. */
15+
export function configureDualStackFallback(): void {
16+
dualStackNet.setDefaultAutoSelectFamilyAttemptTimeout(
17+
DUAL_STACK_ATTEMPT_TIMEOUT_MS,
18+
);
19+
}

src/lib/programs/posthog-doctor/__tests__/fetch.test.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/lib/programs/posthog-doctor/fetch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from 'axios';
22
import { analytics } from '@utils/analytics';
3-
import { handleApiError, posthogApiHttpsAgent } from '@lib/api';
3+
import { handleApiError } from '@lib/api';
44
import { WIZARD_USER_AGENT } from '@lib/constants';
55
import { HealthIssueListResponseSchema, type HealthIssue } from './types';
66

@@ -17,7 +17,6 @@ export async function fetchHealthIssues(
1717
Authorization: `Bearer ${accessToken}`,
1818
'User-Agent': WIZARD_USER_AGENT,
1919
},
20-
httpsAgent: posthogApiHttpsAgent,
2120
});
2221
return HealthIssueListResponseSchema.parse(response.data).results;
2322
} catch (error) {

0 commit comments

Comments
 (0)