Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPLAT-7606 Improve handling of “repeat” errors thrown from components wrapped by an ErrorBoundary #350

Merged
merged 7 commits into from
Oct 8, 2019

Conversation

aaronlademann-wf
Copy link
Contributor

Motivation

While testing the recent 3.0.0-alpha.0 release in a consumer library, a problem was discovered.

Currently, our ErrorBoundary component - when props.fallbackUIRenderer is not specified - will attempt to remount its child component tree when an error is caught. In most cases, this is a sane default - however - if a component in that tree throws every time it mounts - this causes a huge problem, since it creates a sort of "infinite loop" of component throws in componentDidMount -> ErrorBoundary remounts the componnet -> component throws in componentDidMount -> .... This infinite loop has the potential to completely lock up the main CPU thread.

Changes

See code comments for much more in-depth information about what happens when / why

  1. Add some internal component fields to ErrorBoundaryComponent that keep track of the error that was thrown, and the "component stack" from the info object made available from ReactJS when a component wrapped in an error boundary throws.
  2. Use these new fields when a second error is thrown to evaluate whether the second error is identical to the first - and whether it was thrown from the exact same place in the tree.
  3. If it is identical - and the errors happen within 5 seconds of each other, render a non-interactive String representation of the DOM that existed before the error was thrown.
  4. Allow consumers to get ErrorBoundary out of error states by either passing in a new child, or by calling the new reset instance method.
  5. Add lots of tests and docs.

Release Notes

Improve handling of “repeat” errors thrown from components wrapped by an ErrorBoundary component.

Review

See CONTRIBUTING.md for more details on review types (+1 / QA +1 / +10) and code review process.

Please review: @greglittlefield-wf @kealjones-wk @joebingham-wk @sydneyjodon-wk

QA Checklist

  • Tests were updated and provide good coverage of the changeset and other affected code
  • Manual testing was performed if needed

Merge Checklist

While we perform many automated checks before auto-merging, some manual checks are needed:

  • A Client Platform member has reviewed these changes
  • There are no unaddressed comments - this check can be automated if reviewers use the "Request Changes" feature
  • For release PRs - Version metadata in Rosie comment is correct

@aviary-wf
Copy link

aviary-wf commented Oct 3, 2019

Security Insights

The items listed below may not capture all security relevant changes. Before providing a security review, be sure to review the entire PR for security impact.

(3) Security relevant changes were detected
  • Watched keyword innerHtml in lib/src/component/error_boundary.dart line(s) ['323'] added
  • Watched keyword InnerHtml in lib/src/component/error_boundary.dart line(s) ['347'] added
  • Watched keyword dangerouslySetInnerHTML in lib/src/component/error_boundary.dart line(s) ['348'] added
  • Action Items

    • Obtain a security review; reviewer should pay special attention to insights listed above
    • Verify aviary.yaml coverage of security relevant code

    Questions or Comments? Reach out on Slack: #support-infosec.

    @maxwellpeterson-wf
    Copy link
    Member

    +1 security

    • Our CSP protects us from any potential XSS that this allows.

    return (Dom.div()
    ..key = 'ohnoes'
    ..addTestId('ErrorBoundary.unrecoverableErrorInnerHtmlContainerNode')
    ..['dangerouslySetInnerHTML'] = {'__html': _domAtTimeOfError} ?? ''
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    What are the cases when the null check will return ''? Because the Map is always set with a key, won't it always be non-null?

    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Is this supposed to be on _domAtTimeOfError?

    Suggested change
    ..['dangerouslySetInnerHTML'] = {'__html': _domAtTimeOfError} ?? ''
    ..['dangerouslySetInnerHTML'] = {'__html': _domAtTimeOfError ?? ''}

    Maybe we should add a test where the child component is a or renders a fragment, which would represent a case where _domAtTimeOfError would be null:

    ErrorBoundary([ // Can't use variadic children or the fragment will be interpreted as two separate children
      // A list-style "fragment" as the single child.
      [
        (Dom.div()..key = 1)(),
        (FaultyComponent()..key = 2)(),
      ],
    ])

    Or actually, maybe even an empty component would do...

    Copy link
    Contributor Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Nice catch on this typo @joebingham-wk !!!

    Copy link
    Contributor Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Fixed the typo, added edge case test coverage in a9e864e

    test/over_react/component/error_boundary_test.dart Outdated Show resolved Hide resolved
    Copy link
    Contributor

    @greglittlefield-wf greglittlefield-wf left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Mostly nits and then a few questions/comments. Overall, looks really solid!

    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    test/over_react/component/error_boundary_test.dart Outdated Show resolved Hide resolved
    test/over_react/component/error_boundary_test.dart Outdated Show resolved Hide resolved
    test/over_react/component/error_boundary_test.dart Outdated Show resolved Hide resolved
    test/over_react/component/error_boundary_test.dart Outdated Show resolved Hide resolved
    lib/src/component/error_boundary.dart Outdated Show resolved Hide resolved
    Copy link
    Contributor

    @joebingham-wk joebingham-wk left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    +1

    expect(() => component.setState(component.newState()..hasError = true), throws);
    expect(getByTestId(jacket.getInstance(), 'dummyChild'), isNull,
    reason: 'The child component tree should have been removed from the dom');
    expect(jacket.getNode(), hasAttr(defaultTestIdKey, 'ErrorBoundary.unrecoverableErrorInnerHtmlContainerNode'));
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Can we assert here that the static HTML got rendered? I'm not seeing that tested anywhere

    Copy link
    Contributor Author

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    It is tested in the "gracefully handles errors in its tree when props.fallbackUIRenderer is not set" section:

    https://github.com/Workiva/over_react/pull/350/files#diff-820fada62e348ea7fccc19d5cc72d0f6R342

    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Ah, sorry, I missed that. Thanks!

    Copy link
    Contributor

    @greglittlefield-wf greglittlefield-wf left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    +1

    @greglittlefield-wf
    Copy link
    Contributor

    Works well in WVC and over_react example

    QA +1

    @Workiva/release-management-p

    @maxwellpeterson-wf
    Copy link
    Member

    +1 security refresh

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    None yet

    8 participants