From 072b30593ec034d44d023749d1d34a713ab91d32 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 14 Feb 2026 14:21:23 +0900 Subject: [PATCH] fix(parser): wrap parseAnthropicTokenLimitError in try/catch Add outer try/catch to prevent crashes from non-standard error objects returned by proxy providers (e.g., Antigravity). Add parser tests covering edge cases: circular refs, non-object data fields, invalid JSON in responseBody. --- .../parser.test.ts | 97 +++++++++++++++++++ .../parser.ts | 8 ++ 2 files changed, 105 insertions(+) create mode 100644 src/hooks/anthropic-context-window-limit-recovery/parser.test.ts diff --git a/src/hooks/anthropic-context-window-limit-recovery/parser.test.ts b/src/hooks/anthropic-context-window-limit-recovery/parser.test.ts new file mode 100644 index 000000000..d0fa10584 --- /dev/null +++ b/src/hooks/anthropic-context-window-limit-recovery/parser.test.ts @@ -0,0 +1,97 @@ +/// +import { describe, expect, it } from "bun:test" +import { parseAnthropicTokenLimitError } from "./parser" + +describe("parseAnthropicTokenLimitError", () => { + it("#given a standard token limit error string #when parsing #then extracts tokens", () => { + //#given + const error = "prompt is too long: 250000 tokens > 200000 maximum" + + //#when + const result = parseAnthropicTokenLimitError(error) + + //#then + expect(result).not.toBeNull() + expect(result!.currentTokens).toBe(250000) + expect(result!.maxTokens).toBe(200000) + }) + + it("#given a non-token-limit error #when parsing #then returns null", () => { + //#given + const error = { message: "internal server error" } + + //#when + const result = parseAnthropicTokenLimitError(error) + + //#then + expect(result).toBeNull() + }) + + it("#given null input #when parsing #then returns null", () => { + //#given + const error = null + + //#when + const result = parseAnthropicTokenLimitError(error) + + //#then + expect(result).toBeNull() + }) + + it("#given a proxy error with non-standard structure #when parsing #then returns null without crashing", () => { + //#given + const proxyError = { + data: [1, 2, 3], + error: "string-not-object", + message: "Failed to process error response", + } + + //#when + const result = parseAnthropicTokenLimitError(proxyError) + + //#then + expect(result).toBeNull() + }) + + it("#given a circular reference error #when parsing #then returns null without crashing", () => { + //#given + const circular: Record = { message: "prompt is too long" } + circular.self = circular + + //#when + const result = parseAnthropicTokenLimitError(circular) + + //#then + expect(result).not.toBeNull() + }) + + it("#given an error where data.responseBody has invalid JSON #when parsing #then handles gracefully", () => { + //#given + const error = { + data: { responseBody: "not valid json {{{" }, + message: "prompt is too long with 300000 tokens exceeds 200000", + } + + //#when + const result = parseAnthropicTokenLimitError(error) + + //#then + expect(result).not.toBeNull() + expect(result!.currentTokens).toBe(300000) + expect(result!.maxTokens).toBe(200000) + }) + + it("#given an error with data as a string (not object) #when parsing #then does not crash", () => { + //#given + const error = { + data: "some-string-data", + message: "token limit exceeded", + } + + //#when + const result = parseAnthropicTokenLimitError(error) + + //#then + expect(result).not.toBeNull() + }) +}) diff --git a/src/hooks/anthropic-context-window-limit-recovery/parser.ts b/src/hooks/anthropic-context-window-limit-recovery/parser.ts index dda87bbd5..451d1ab18 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/parser.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/parser.ts @@ -74,6 +74,14 @@ function isTokenLimitError(text: string): boolean { } export function parseAnthropicTokenLimitError(err: unknown): ParsedTokenLimitError | null { + try { + return parseAnthropicTokenLimitErrorUnsafe(err) + } catch { + return null + } +} + +function parseAnthropicTokenLimitErrorUnsafe(err: unknown): ParsedTokenLimitError | null { if (typeof err === "string") { if (err.toLowerCase().includes("non-empty content")) { return {