Skip to content

"Copy diagnostics" can throw an unhandled rejection inside the error screen itself #32

Description

@PAULAUV
    navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2))
    this.setState({ copied: true })
    setTimeout(() => this.setState({ copied: false }), 2000)

(frontend/src/components/ErrorBoundary.tsx:85-87)

Three issues in three lines, in the one component that must never fail.

1. The promise is not awaited and has no .catch. navigator.clipboard.writeText rejects when the document is not focused, when the Permissions API denies clipboard-write, or in any non-secure context. navigator.clipboard is undefined entirely on plain HTTP origins other than localhost — which is precisely the deployment shape README.md:255-258 describes for this RC (an internal host, one frontend process). On such a host this line throws TypeError: Cannot read properties of undefined (reading 'writeText') synchronously, inside a click handler rendered by the error boundary.

Trigger: hit any app error while browsing FAROS over http://<internal-host>:5176, then click "Copy Diagnostics".
Observed: nothing is copied, and the button still flips to the "copied!" state (step 2 below runs regardless). The user believes they have the diagnostics on their clipboard and pastes stale content into the bug report.
Expected: either it copies, or it tells the user it could not.

2. copied: true is set unconditionally, before the write is known to have succeeded. It should be set in the .then.

3. setTimeout with no stored handle, firing setState 2 seconds later on a component that may have been unmounted by handleReset — which navigates the whole document at ErrorBoundary.tsx:51.

Suggested:

handleCopyDiagnostics = async () => {
  const text = JSON.stringify(diagnostics, null, 2)
  try {
    if (!navigator.clipboard) throw new Error('clipboard unavailable')
    await navigator.clipboard.writeText(text)
    this.setState({ copied: true })
    this.copyTimer = window.setTimeout(() => this.setState({ copied: false }), 2000)
  } catch {
    this.setState({ copyFailed: true })   // render a selectable <textarea> fallback
  }
}

componentWillUnmount() { window.clearTimeout(this.copyTimer) }

The <textarea> fallback is the part I would push hardest for. This button exists so that users can hand you a stack trace; on the exact deployment topology this project targets, it currently cannot.

Separately, and more debatable: handleReset at ErrorBoundary.tsx:44-52 clears the error state and then does window.location.href = '/', which is a full document reload that discards the React tree, the QueryClient cache from main.tsx:8, and every lazy chunk. Since the state reset immediately preceding it is thrown away by the navigation, one of the two is redundant. If the intent is "recover in place", use navigate('/') and drop the assignment; if the intent is "nuke everything", drop the setState and say so in a comment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions