From 6d9c5d95438e81c75d2fe6f2e4284b6a16e30bea Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 30 May 2026 04:11:01 +0800 Subject: [PATCH] fix(core): harden context error text collection --- .../core/src/utils/contextLengthError.test.ts | 41 ++++++++++++++++++ packages/core/src/utils/contextLengthError.ts | 42 +++++++++++++++++-- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/core/src/utils/contextLengthError.test.ts b/packages/core/src/utils/contextLengthError.test.ts index 0bd7a104148..186f8d40598 100644 --- a/packages/core/src/utils/contextLengthError.test.ts +++ b/packages/core/src/utils/contextLengthError.test.ts @@ -116,4 +116,45 @@ describe('contextLengthError', () => { expect(info.isExceeded).toBe(false); }); + + it('skips accessor properties that throw while collecting error text', () => { + const error = new Error('Connection error.'); + + Object.defineProperty(error, 'name', { + enumerable: true, + get() { + throw new TypeError('Value of "this" must be of DOMException'); + }, + }); + Object.defineProperty(error, 'details', { + enumerable: true, + get() { + throw new TypeError('Value of "this" must be of DOMException'); + }, + }); + + const info = getContextLengthExceededInfo(error); + + expect(info.isExceeded).toBe(false); + expect(info.message).toContain('Connection error.'); + }); + + it('skips throwing accessors on plain objects', () => { + const errorLike: Record = {}; + Object.defineProperty(errorLike, 'detail', { + enumerable: true, + get() { + throw new TypeError('accessor refused'); + }, + }); + Object.defineProperty(errorLike, 'message', { + enumerable: true, + value: 'context_length_exceeded: too many tokens', + }); + + const info = getContextLengthExceededInfo(errorLike); + + expect(info.isExceeded).toBe(true); + expect(info.message).toContain('context_length_exceeded'); + }); }); diff --git a/packages/core/src/utils/contextLengthError.ts b/packages/core/src/utils/contextLengthError.ts index ce0d8ff68b6..5cfe2fbc4a4 100644 --- a/packages/core/src/utils/contextLengthError.ts +++ b/packages/core/src/utils/contextLengthError.ts @@ -103,6 +103,32 @@ function tryParseEmbeddedJson(text: string): unknown | undefined { } } +function safeReadProperty(value: object, key: string): unknown { + try { + return (value as Record)[key]; + } catch { + return undefined; + } +} + +function enumerableValues(value: object): unknown[] { + try { + return Object.values(value); + } catch { + try { + const descriptors = Object.getOwnPropertyDescriptors(value); + return Object.values(descriptors) + .filter( + (descriptor): descriptor is PropertyDescriptor & { value: unknown } => + 'value' in descriptor && descriptor.enumerable === true, + ) + .map((descriptor) => descriptor.value); + } catch { + return []; + } + } +} + function collectStrings( value: unknown, seen: Set, @@ -135,11 +161,21 @@ function collectStrings( const strings: string[] = []; if (value instanceof Error) { - strings.push(value.name, value.message); - strings.push(...collectStrings(value.cause, seen, depth + 1)); + const name = safeReadProperty(value, 'name'); + const message = safeReadProperty(value, 'message'); + + if (typeof name === 'string') { + strings.push(name); + } + if (typeof message === 'string') { + strings.push(message); + } + strings.push( + ...collectStrings(safeReadProperty(value, 'cause'), seen, depth + 1), + ); } - for (const [, nested] of Object.entries(value)) { + for (const nested of enumerableValues(value)) { strings.push(...collectStrings(nested, seen, depth + 1)); }