Merge pull request #3204 from jeremymcs/fix/stream-re-catch-all-json-parse
fix(error-classifier): catch-all V8 JSON.parse pattern replaces STREAM_RE whack-a-mole
This commit is contained in:
commit
03bb723dfb
3 changed files with 58 additions and 4 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -2,6 +2,13 @@
|
|||
# ── Compiled test output ──
|
||||
dist-test/
|
||||
|
||||
# ── Compiled output in src/ (should only contain .ts source) ──
|
||||
src/**/*.js
|
||||
src/**/*.js.map
|
||||
src/**/*.d.ts
|
||||
src/**/*.d.ts.map
|
||||
!src/**/*.test.js
|
||||
|
||||
# ── GSD project state (development-only, lives in worktree branches) ──
|
||||
package-lock.json
|
||||
.claude/
|
||||
|
|
@ -42,6 +49,9 @@ tmp/
|
|||
packages/*/dist/
|
||||
packages/*/node_modules/
|
||||
|
||||
# ── Scratch/WIP files ──
|
||||
preflight-script.ts
|
||||
|
||||
# ── GSD baseline (auto-generated) ──
|
||||
dist/
|
||||
!/pkg/dist/modes/
|
||||
|
|
@ -55,6 +65,7 @@ TODOS.md
|
|||
.planning/
|
||||
.audits/
|
||||
docs/coherence-audit/
|
||||
.plans/
|
||||
|
||||
# ── GSD project state (per-worktree, never committed) ──
|
||||
.gsd/
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fet
|
|||
const SERVER_RE = /internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable/i;
|
||||
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
||||
const CONNECTION_RE = /terminated|connection.?refused|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
||||
const STREAM_RE = /Unexpected end of JSON|Unexpected token.*JSON|Expected.*in JSON|Unterminated.*in JSON|SyntaxError.*JSON/i;
|
||||
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
||||
// This eliminates the need to enumerate every error message variant individually.
|
||||
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
||||
const RESET_DELAY_RE = /reset in (\d+)s/i;
|
||||
|
||||
/**
|
||||
|
|
@ -91,9 +93,6 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
|
|||
}
|
||||
|
||||
// 4. Stream truncation — downstream symptom of connection drop
|
||||
// Checked before server/connection because JSON parse errors can contain
|
||||
// substrings like "position 500" (matches SERVER_RE) or "Unterminated"
|
||||
// (matches CONNECTION_RE's "terminated" pattern).
|
||||
if (STREAM_RE.test(errorMsg)) {
|
||||
return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,3 +82,47 @@ test("#2572: 'SyntaxError' with JSON context (truncated stream) is transient", (
|
|||
assert.equal(isTransient(result), true, "'SyntaxError...JSON' should be transient");
|
||||
assert.equal(result.kind, "stream", "JSON parse errors are stream kind");
|
||||
});
|
||||
|
||||
// --- Catch-all: all V8 JSON.parse variants matched by "in JSON at position" ---
|
||||
|
||||
test("V8 JSON.parse: 'No number after minus sign in JSON' is transient (#2882)", () => {
|
||||
const result = classifyError("No number after minus sign in JSON at position 42");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse: 'Expected property value after colon' is transient", () => {
|
||||
const result = classifyError("Expected ',' or '}' after property value in JSON at position 108");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse: 'Bad control character in string literal' is transient", () => {
|
||||
const result = classifyError("Bad control character in string literal in JSON at position 5");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse: 'Bad escaped character' is transient", () => {
|
||||
const result = classifyError("Bad escaped character in JSON at position 17");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse: 'Unexpected number' is transient", () => {
|
||||
const result = classifyError("Unexpected number in JSON at position 0");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse: 'Unexpected string' is transient", () => {
|
||||
const result = classifyError("Unexpected string in JSON at position 12");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
||||
test("V8 JSON.parse with line/column suffix is transient", () => {
|
||||
const result = classifyError("Unexpected token x in JSON at position 99 (line 3 column 14)");
|
||||
assert.equal(isTransient(result), true);
|
||||
assert.equal(result.kind, "stream");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue