From d0c99b279d5bb38fb2f6d6fe998489149f8ca004 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 22 Mar 2023 12:10:02 +0100 Subject: [PATCH] Be more defensive when recursing through error cause chain --- packages/react/src/errorboundary.tsx | 18 ++++++++-- packages/react/test/errorboundary.test.tsx | 39 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index d7312fdeffbb..377eedc7fd47 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -67,10 +67,22 @@ const INITIAL_STATE = { }; function setCause(error: Error & { cause?: Error }, cause: Error): void { - if (error.cause) { - return setCause(error.cause, cause); + const seenErrors = new Map(); + + function recurse(error: Error & { cause?: Error }, cause: Error): void { + // If we've already seen the error, there is a recursive loop somewhere in the error's + // cause chain. Let's just bail out then to prevent a stack overflow. + if (seenErrors.has(error)) { + return; + } + if (error.cause) { + seenErrors.set(error, true); + return recurse(error.cause, cause); + } + error.cause = cause; } - error.cause = cause; + + recurse(error, cause); } /** diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 3f9284d03efa..251112242313 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -341,6 +341,45 @@ describe('ErrorBoundary', () => { expect(cause.message).toEqual(thirdError.message); }); + it('handles when `error.cause` is recursive', () => { + const mockOnError = jest.fn(); + + function CustomBam(): JSX.Element { + const firstError = new Error('bam'); + const secondError = new Error('bam2'); + // @ts-ignore Need to set cause on error + firstError.cause = secondError; + // @ts-ignore Need to set cause on error + secondError.cause = firstError; + throw firstError; + } + + render( + You have hit an error

} onError={mockOnError} errorComp={}> +

children

+
, + ); + + expect(mockOnError).toHaveBeenCalledTimes(0); + expect(mockCaptureException).toHaveBeenCalledTimes(0); + + const btn = screen.getByTestId('errorBtn'); + fireEvent.click(btn); + + expect(mockCaptureException).toHaveBeenCalledTimes(1); + expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), { + contexts: { react: { componentStack: expect.any(String) } }, + }); + + expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]); + + const error = mockCaptureException.mock.calls[0][0]; + const cause = error.cause; + // We need to make sure that recursive error.cause does not cause infinite loop + expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack); + expect(cause.name).not.toContain('React ErrorBoundary'); + }); + it('calls `beforeCapture()` when an error occurs', () => { const mockBeforeCapture = jest.fn();