From e0e72ee250eb7507b8143fd14b174a2a460d2c89 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 30 Jan 2026 14:35:24 -0500 Subject: [PATCH 1/2] test: add reproduction test --- packages/core/test/lib/client.test.ts | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 09ec34bf4fcc..028c25d1b840 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -2007,6 +2007,36 @@ describe('Client', () => { }); }); + test('client-level event processor that throws on all events does not cause infinite recursion', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + const client = new TestClient(options); + + let processorCallCount = 0; + // Add processor at client level - this runs on ALL events including internal exceptions + client.addEventProcessor(() => { + processorCallCount++; + throw new Error('Processor always throws'); + }); + + client.captureMessage('test message'); + + // Without the fix, this would cause infinite recursion: + // 1. captureMessage -> processor throws -> captureException(__sentry__: true) + // 2. captureException -> processor throws again -> captureException(__sentry__: true) + // 3. Infinite loop... + // + // With the fix, the processor is called once for the original message, + // and the internal exception event skips event processors entirely. + expect(processorCallCount).toBe(1); + + // Verify the processor error was captured and sent + expect(TestClient.instance!.event!.exception!.values![0]).toStrictEqual({ + type: 'Error', + value: 'Processor always throws', + mechanism: { type: 'internal', handled: false }, + }); + }); + test('records events dropped due to `sampleRate` option', () => { expect.assertions(1); From f420e5a259846cf6fd442d9a475b601f19752810 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 30 Jan 2026 14:48:53 -0500 Subject: [PATCH 2/2] fix(core): Prevent infinite recursion in event processing for internal exceptions --- packages/core/src/utils/prepareEvent.ts | 7 ++++++- packages/core/test/lib/client.test.ts | 9 ++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 3a127d332686..6528873c3dee 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -11,6 +11,7 @@ import { addExceptionMechanism, uuid4 } from './misc'; import { normalize } from './normalize'; import { applyScopeDataToEvent, getCombinedScopeData } from './scopeData'; import { truncate } from './string'; +import { resolvedSyncPromise } from './syncpromise'; import { dateTimestampInSeconds } from './time'; /** @@ -93,7 +94,11 @@ export function prepareEvent( ...data.eventProcessors, ]; - const result = notifyEventProcessors(eventProcessors, prepared, hint); + // Skip event processors for internal exceptions to prevent recursion + const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true; + const result = isInternalException + ? resolvedSyncPromise(prepared) + : notifyEventProcessors(eventProcessors, prepared, hint); return result.then(evt => { if (evt) { diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 028c25d1b840..21aab0ea609b 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -2020,13 +2020,8 @@ describe('Client', () => { client.captureMessage('test message'); - // Without the fix, this would cause infinite recursion: - // 1. captureMessage -> processor throws -> captureException(__sentry__: true) - // 2. captureException -> processor throws again -> captureException(__sentry__: true) - // 3. Infinite loop... - // - // With the fix, the processor is called once for the original message, - // and the internal exception event skips event processors entirely. + // Should be called once for the original message + // internal exception events skips event processors entirely. expect(processorCallCount).toBe(1); // Verify the processor error was captured and sent