Skip to content

Commit 996c6ca

Browse files
committed
Allow viable upstream connection attempts
1 parent e6647d6 commit 996c6ca

7 files changed

Lines changed: 115 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Release tags use the form `vX.Y.Z` and match `package.json`. GitHub Releases car
1414
### Fixed
1515

1616
- Proxy-generated HTTP 502/503/504 connection-reset responses now use the same retry, no-lock, and route-fallback behavior as thrown socket failures.
17+
- Outbound connections now allow five seconds per resolved address instead of Node's 250-millisecond default, preventing healthy but moderately latent TCP handshakes from being misreported as `ETIMEDOUT` across every ChatGPT address.
1718

1819
## [0.5.11] - 2026-08-12
1920

src/cli/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33

44
const fs = require("node:fs");
55
const path = require("node:path");
6+
const { configureNetworkDefaults } = require("../lib/network");
67
const { createHeadlessRuntime, createProcessLock, defaultUserData } = require("../lib/headless-runtime");
78
const { createPrompts } = require("./prompts");
89
const { runFirstSetup } = require("./setup");
910
const packageJson = require("../../package.json");
1011

12+
configureNetworkDefaults();
13+
1114
const HELP = `ReRouted ${packageJson.version}
1215
1316
Usage:

src/lib/network.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use strict";
2+
3+
const net = require("node:net");
4+
5+
// Node's 250 ms default is too short for otherwise healthy TCP paths and can
6+
// exhaust every resolved address before TLS has a chance to start. Keep
7+
// dual-stack address fallback, but give each connection attempt a realistic
8+
// window.
9+
const ADDRESS_ATTEMPT_TIMEOUT_MS = 5_000;
10+
11+
function configureNetworkDefaults(network = net) {
12+
if (typeof network.setDefaultAutoSelectFamily === "function") {
13+
network.setDefaultAutoSelectFamily(true);
14+
}
15+
if (typeof network.setDefaultAutoSelectFamilyAttemptTimeout === "function") {
16+
network.setDefaultAutoSelectFamilyAttemptTimeout(ADDRESS_ATTEMPT_TIMEOUT_MS);
17+
}
18+
return {
19+
autoSelectFamily:
20+
typeof network.getDefaultAutoSelectFamily === "function"
21+
? network.getDefaultAutoSelectFamily()
22+
: null,
23+
attemptTimeoutMs:
24+
typeof network.getDefaultAutoSelectFamilyAttemptTimeout === "function"
25+
? network.getDefaultAutoSelectFamilyAttemptTimeout()
26+
: null,
27+
};
28+
}
29+
30+
module.exports = { ADDRESS_ATTEMPT_TIMEOUT_MS, configureNetworkDefaults };

src/lib/router.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,15 +759,17 @@ function isAbortError(err) {
759759
function transportErrorMessage(error) {
760760
const parts = [];
761761
const seen = new Set();
762-
let current = error;
763-
for (let depth = 0; current && depth < 4; depth += 1) {
764-
if (seen.has(current)) break;
762+
const pending = [error];
763+
for (let inspected = 0; pending.length && inspected < 12; inspected += 1) {
764+
const current = pending.shift();
765+
if (!current || seen.has(current)) continue;
765766
seen.add(current);
766767
const code = typeof current.code === "string" ? current.code.trim() : "";
767768
const message = String(current.message || current).trim();
768769
const detail = code && !message.includes(code) ? `${code}: ${message}` : message;
769770
if (detail && !parts.includes(detail)) parts.push(detail);
770-
current = current.cause;
771+
if (current.cause) pending.push(current.cause);
772+
if (Array.isArray(current.errors)) pending.push(...current.errors);
771773
}
772774
return parts.join("; ") || "Upstream connection failed";
773775
}

src/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const path = require("node:path");
44
const fs = require("node:fs");
5+
const { configureNetworkDefaults } = require("./lib/network");
56
const {
67
app,
78
BrowserWindow,
@@ -16,6 +17,8 @@ const {
1617
powerMonitor,
1718
} = require("electron");
1819

20+
configureNetworkDefaults();
21+
1922
const { createStore } = require("./lib/store");
2023
const { createRouter } = require("./lib/router");
2124
const { createGateway } = require("./lib/gateway");

tests/network.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
3+
const { describe, it } = require("node:test");
4+
const assert = require("node:assert/strict");
5+
6+
const {
7+
ADDRESS_ATTEMPT_TIMEOUT_MS,
8+
configureNetworkDefaults,
9+
} = require("../src/lib/network");
10+
11+
describe("network defaults", () => {
12+
it("keeps address-family fallback and replaces Node's 250 ms connection window", () => {
13+
const state = { autoSelectFamily: false, attemptTimeoutMs: 250 };
14+
const network = {
15+
setDefaultAutoSelectFamily(value) {
16+
state.autoSelectFamily = value;
17+
},
18+
getDefaultAutoSelectFamily() {
19+
return state.autoSelectFamily;
20+
},
21+
setDefaultAutoSelectFamilyAttemptTimeout(value) {
22+
state.attemptTimeoutMs = value;
23+
},
24+
getDefaultAutoSelectFamilyAttemptTimeout() {
25+
return state.attemptTimeoutMs;
26+
},
27+
};
28+
29+
assert.deepEqual(configureNetworkDefaults(network), {
30+
autoSelectFamily: true,
31+
attemptTimeoutMs: ADDRESS_ATTEMPT_TIMEOUT_MS,
32+
});
33+
assert.equal(ADDRESS_ATTEMPT_TIMEOUT_MS, 5_000);
34+
});
35+
});

tests/router-fallback.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,43 @@ describe("Claude Code canonical named routes", () => {
320320
});
321321

322322
describe("same-provider OAuth account fallback", () => {
323+
it("logs the individual socket causes hidden inside a Node AggregateError", async () => {
324+
const store = createStore(tmpConfig());
325+
store.seed({ providers: [chatgptAccount("prov_a", "token-a", 100)] });
326+
const logger = captureLogger();
327+
const router = createRouter({
328+
store,
329+
logger,
330+
transportRetryDelayMs: 0,
331+
transportRetryAttempts: 0,
332+
fetchImpl: async () => {
333+
const timeout = Object.assign(new Error("connect timed out 172.64.155.209:443"), {
334+
code: "ETIMEDOUT",
335+
});
336+
const unreachable = Object.assign(new Error("connect unreachable 2606:4700::1:443"), {
337+
code: "ENETUNREACH",
338+
});
339+
throw new TypeError("fetch failed", {
340+
cause: new AggregateError([timeout, unreachable], "connection attempts failed"),
341+
});
342+
},
343+
});
344+
345+
const result = await router.chatCompletions({
346+
body: {
347+
model: "chatgpt/gpt-5.4",
348+
messages: [{ role: "user", content: "hello" }],
349+
stream: false,
350+
},
351+
});
352+
353+
assert.equal(result.ok, false);
354+
const failure = logger.entries.find((entry) => entry.meta?.event === "account_failure");
355+
assert.match(failure.meta.transportCause, /ETIMEDOUT.*172\.64\.155\.209/);
356+
assert.match(failure.meta.transportCause, /ENETUNREACH.*2606:4700/);
357+
assert.deepEqual(store.load().providers[0].modelLocks, {});
358+
});
359+
323360
it("retries an HTTP proxy connection reset without locking the account", async () => {
324361
const store = createStore(tmpConfig());
325362
store.seed({ providers: [chatgptAccount("prov_a", "token-a", 100)] });

0 commit comments

Comments
 (0)