Skip to content

Commit e6647d6

Browse files
committed
Fix proxy transport responses locking accounts
1 parent 1428e3e commit e6647d6

5 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Release tags use the form `vX.Y.Z` and match `package.json`. GitHub Releases car
99

1010
## [Unreleased]
1111

12+
## [0.5.12] - 2026-08-12
13+
14+
### Fixed
15+
16+
- 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+
1218
## [0.5.11] - 2026-08-12
1319

1420
### Fixed

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@gitcommit90/rerouted",
33
"productName": "ReRouted",
4-
"version": "0.5.11",
4+
"version": "0.5.12",
55
"description": "A local AI router for connected accounts, models, named routes, and automatic fallback.",
66
"author": "gitcommit90",
77
"license": "MIT",

‎src/lib/router.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,14 @@ function classifyFailure(status, errorText) {
274274
text
275275
);
276276
if (request) return { eligible: false, kind: "request", defaultCooldownMs: 0 };
277+
const transportResponse =
278+
[502, 503, 504].includes(Number(status)) &&
279+
/upstream connect error|disconnect\/reset before headers|reset reason:\s*(?:connection|remote)|connection (?:termination|reset|refused)|socket hang up|tls (?:handshake|connection)|(?:dns|name) resolution|no healthy upstream|upstream (?:request )?timeout|gateway timeout/.test(
280+
text
281+
);
282+
if (transportResponse) {
283+
return { eligible: false, kind: "transport", defaultCooldownMs: 0 };
284+
}
277285
const capability =
278286
(status === 400 || status === 404 || status === 422) &&
279287
(/(?:unsupported|invalid|unknown)[ _-]?model|model[ _-]?(?:not[ _-]?found|unsupported|unavailable)/.test(
@@ -1014,6 +1022,7 @@ function createRouter({
10141022
failureKind: classification.kind,
10151023
defaultCooldownMs: classification.defaultCooldownMs,
10161024
resetAt: inspected.failure.resetAt,
1025+
transportError: classification.kind === "transport",
10171026
};
10181027
}
10191028
if (inspected.openAiJson) {
@@ -1076,6 +1085,7 @@ function createRouter({
10761085
failureKind: classification.kind,
10771086
defaultCooldownMs: classification.defaultCooldownMs,
10781087
resetAt: parseResetHint(res, text),
1088+
transportError: classification.kind === "transport",
10791089
};
10801090
}
10811091

@@ -1186,6 +1196,7 @@ function createRouter({
11861196
failureKind: classification.kind,
11871197
defaultCooldownMs: classification.defaultCooldownMs,
11881198
resetAt: parseResetHint(res, JSON.stringify(raw)),
1199+
transportError: classification.kind === "transport",
11891200
};
11901201
}
11911202
let openAiJson = raw;

‎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("retries an HTTP proxy connection reset without locking the account", async () => {
324+
const store = createStore(tmpConfig());
325+
store.seed({ providers: [chatgptAccount("prov_a", "token-a", 100)] });
326+
const logger = captureLogger();
327+
let calls = 0;
328+
const router = createRouter({
329+
store,
330+
logger,
331+
transportRetryDelayMs: 0,
332+
fetchImpl: async () => {
333+
calls += 1;
334+
if (calls < 3) {
335+
return new Response(
336+
"upstream connect error or disconnect/reset before headers. reset reason: connection termination",
337+
{ status: 503 }
338+
);
339+
}
340+
return responsesSuccessResponse("recovered");
341+
},
342+
});
343+
344+
const result = await router.chatCompletions({
345+
body: {
346+
model: "chatgpt/gpt-5.4",
347+
messages: [{ role: "user", content: "hello" }],
348+
stream: false,
349+
},
350+
});
351+
352+
assert.equal(result.ok, true, JSON.stringify(result.error));
353+
assert.equal(calls, 3);
354+
assert.deepEqual(store.load().providers[0].modelLocks, {});
355+
const retries = logger.entries.filter((entry) => entry.meta?.event === "transport_retry_scheduled");
356+
assert.equal(retries.length, 2);
357+
assert.match(retries[0].meta.error, /disconnect\/reset before headers/);
358+
});
359+
323360
it("retries transport failures 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)