fix: suppress internal error details from users in production mode#7434
fix: suppress internal error details from users in production mode#7434JohnMcLear wants to merge 1 commit intoether:developfrom
Conversation
In production mode (NODE_ENV=production), the client-side error handler now shows a generic "reload the page" message with just the ErrorId instead of leaking internal details like error messages, file paths, line numbers, stack traces, and user agent strings. In development mode, the full error details are still shown for debugging. The basic_error_handler (pre-initialization errors) now always shows a generic message and logs details to the console instead of displaying them in the DOM. The server-side jserror endpoint still receives full error details for server-side logging — only the user-facing display is suppressed. Fixes: ether#5765 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review Summary by QodoSuppress internal error details from users in production mode
WalkthroughsDescription• Hide sensitive error details from users in production mode • Show only generic reload message with ErrorId for correlation • Log full error details to console instead of DOM display • Preserve server-side error logging for admin debugging Diagramflowchart LR
A["Error Occurs"] --> B{Production Mode?}
B -->|Yes| C["Show Generic Message + ErrorId"]
B -->|No| D["Show Full Error Details"]
C --> E["Log Details to Console"]
D --> E
E --> F["Send to Server jserror Endpoint"]
File Changes1. src/static/js/basic_error_handler.ts
|
Code Review by Qodo
1. isProduction error UI untested
|
| // In production mode, hide internal error details (file paths, line numbers, | ||
| // error messages) from end users. Only show a generic reload message. | ||
| // See https://github.com/ether/etherpad-lite/issues/5765 | ||
| const isProduction = (window as any).clientVars?.mode === 'production'; | ||
|
|
||
| const errorMsg = isProduction | ||
| ? [ | ||
| $('<p>') | ||
| .append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')), | ||
| $('<p>') | ||
| .text('If the problem persists, please contact your webmaster.') | ||
| .append($('<br>')) | ||
| .append($('<span>').css('font-size', '.8em').text(`ErrorId: ${errorId}`)), | ||
| ] | ||
| : (() => { | ||
| const txt = document.createTextNode.bind(document); | ||
| return [ | ||
| $('<p>') | ||
| .append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')), | ||
| $('<p>') | ||
| .text('If the problem persists, please send this error message to your webmaster:'), | ||
| $('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em') | ||
| .append($('<b>').addClass('error-msg').text(msg)).append($('<br>')) | ||
| .append(txt(`at ${url} at line ${linenumber}`)).append($('<br>')) | ||
| .append(txt(`ErrorId: ${errorId}`)).append($('<br>')) | ||
| .append(txt(type)).append($('<br>')) | ||
| .append(txt(`URL: ${window.location.href}`)).append($('<br>')) | ||
| .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')), | ||
| ]; | ||
| })(); |
There was a problem hiding this comment.
1. isproduction error ui untested 📘 Rule violation ☼ Reliability
The PR changes user-facing error sanitization behavior but does not include an automated regression test to ensure the sanitized message remains enforced. Reverting these changes would not be caught by CI, risking re-exposure of internal error details.
Agent Prompt
## Issue description
A bug fix that suppresses internal error details from end users was merged without an automated regression test, so CI will not fail if the change is reverted.
## Issue Context
The production-mode `globalExceptionHandler` output is now sanitized, and the page-load error handler no longer renders stack/file details in the DOM.
## Fix Focus Areas
- src/static/js/pad_utils.ts[428-457]
- src/static/js/basic_error_handler.ts[26-34]
- src/tests/frontend-new/specs/error_sanitization.spec.ts[1-200]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const isProduction = (window as any).clientVars?.mode === 'production'; | ||
|
|
||
| const errorMsg = isProduction | ||
| ? [ | ||
| $('<p>') | ||
| .append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')), | ||
| $('<p>') | ||
| .text('If the problem persists, please contact your webmaster.') | ||
| .append($('<br>')) | ||
| .append($('<span>').css('font-size', '.8em').text(`ErrorId: ${errorId}`)), | ||
| ] | ||
| : (() => { | ||
| const txt = document.createTextNode.bind(document); | ||
| return [ | ||
| $('<p>') | ||
| .append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')), | ||
| $('<p>') | ||
| .text('If the problem persists, please send this error message to your webmaster:'), | ||
| $('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em') | ||
| .append($('<b>').addClass('error-msg').text(msg)).append($('<br>')) | ||
| .append(txt(`at ${url} at line ${linenumber}`)).append($('<br>')) | ||
| .append(txt(`ErrorId: ${errorId}`)).append($('<br>')) | ||
| .append(txt(type)).append($('<br>')) | ||
| .append(txt(`URL: ${window.location.href}`)).append($('<br>')) | ||
| .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')), | ||
| ]; | ||
| })(); |
There was a problem hiding this comment.
2. Mode check leaks details 🐞 Bug ⛨ Security
globalExceptionHandler decides whether to hide details with `window.clientVars?.mode === 'production', but window.clientVars is initially created without mode and mode` is only populated after the async CLIENT_VARS handshake. Errors before that (or if NODE_ENV is not exactly 'production') will incorrectly take the non-production path and show internal details to end users.
Agent Prompt
## Issue description
`setupGlobalExceptionHandler()` currently treats "production" as a strict `clientVars.mode === 'production'` check. Because `window.clientVars.mode` is not available until after the async CLIENT_VARS handshake (and is set from `process.env.NODE_ENV` without a default), the code can incorrectly show the detailed error message UI to end users.
## Issue Context
- `pad.init()` installs the global exception handler before awaiting `handshake()`.
- The bootstrap scripts initialize `window.clientVars` with only `randomVersionString` (no `mode`).
- Server sets `clientVars.mode = process.env.NODE_ENV`, which can be `undefined` or other strings (e.g., `test`).
## Fix Focus Areas
- src/static/js/pad_utils.ts[420-457]
- src/static/js/pad.ts[410-423]
- src/templates/padBootstrap.js[6-10]
- src/templates/padViteBootstrap.js[4-8]
- src/node/handler/PadMessageHandler.ts[1060-1062]
## Suggested fix
- Change the logic to a secure default, e.g. compute `showDetails = window.clientVars?.mode === 'development'` and use `showDetails ? detailed : generic`.
- (Optional but best) Ensure `mode` is present as early as possible (e.g., include `mode` in the initial `window.clientVars` object in the bootstrap templates) so development builds still show full details even before the handshake.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
pad_utils.ts(globalExceptionHandler): In production mode (window.clientVars.mode === 'production'), the gritter error popup now shows only a generic reload message with the ErrorId. Error messages, file paths, line numbers, stack traces, and user agent strings are hidden. In development mode, full details are still shown.basic_error_handler.ts(page load errors): No longer renders error details, stack traces, or file paths in the DOM. Logs them toconsole.errorinstead. This handler runs beforeclientVarsis available so it always uses the generic message.$.post('../jserror', ...)still sends full error details to the server'sjserrorendpoint for server-side logging.What users see
Production (before):
Production (after):
The ErrorId is preserved so users can reference it when contacting their admin, and admins can correlate it with server logs.
Test plan
Fixes #5765
🤖 Generated with Claude Code