ErrorPage.java| Commit | Message |
|---|---|
9ebb88522 | 2016-07-18 Initial commit |
Original code from Kai Reinhard. Has survived the Wicket→React migration — still active in the Wicket module.
public static String getExceptionMessage(AbstractSecuredBasePage securedPage,
ProjectForgeException exception, boolean doLog) {
if (exception instanceof UserException) { // Category 1: user-facing errors
return securedPage.translateParams(ex.getI18nKey(), ex.getMsgParams(), ex.getParams());
} else if (exception instanceof InternalErrorException) { // Category 2: internal errors
return securedPage.translateParams(ex.getI18nKey(), ex.getMsgParams(), ex.getParams());
} else if (exception instanceof AccessException) { // Category 3: access denied
return securedPage.translateParams(ex.getI18nKey(), ex.getMessageArgs(), ex.getParams());
}
throw new UnsupportedOperationException("...add unknown ProjectForgeException here!", exception);
}public ErrorPage(final Throwable throwable) {
// Default: show generic error with feedback form
errorMessage = getString("errorpage.unknownError");
showFeedback = true;
if (throwable != null) {
rootCause = ExceptionHelper.getRootCause(throwable);
// PATH A: ProjectForgeException — translated message
if (rootCause instanceof ProjectForgeException) {
errorMessage = getExceptionMessage(this, (ProjectForgeException) rootCause, true);
// PATH B: ServletException — log + "contact dev team" message
} else if (throwable instanceof ServletException) {
messageNumber = String.valueOf(System.currentTimeMillis());
log.error("Message #" + messageNumber + ": " + throwable.getMessage(), throwable);
errorMessage = getLocalizedMessage(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM, messageNumber);
// PATH C: PageExpiredException — session timeout (no feedback form)
} else if (throwable instanceof PageExpiredException) {
showFeedback = false; // Don't ask user for feedback on timeout
errorMessage = getString("message.wicket.pageExpired");
// PATH D: Everything else — generic "contact dev team"
} else {
messageNumber = String.valueOf(System.currentTimeMillis());
log.error("Message #" + messageNumber + ": " + throwable.getMessage(), throwable);
errorMessage = getLocalizedMessage(UserException.I18N_KEY_PLEASE_CONTACT_DEVELOPER_TEAM, messageNumber);
}
}
}PATH A — ProjectForge exceptions (lines 130-131): The happy path for known errors. The root cause is unwrapped via ExceptionHelper.getRootCause() (strips ServletException/Spring wrappers), and the message is translated via i18n. The user sees a helpful message in their language.
PATH B — ServletException (lines 132-138): Container-level errors (Tomcat filter chain failures, request parsing errors). Gets a messageNumber — System.currentTimeMillis() as a string. This is the unique error ID shown to the user as "Please contact the developer team with message #1712345678901". The number is a timestamp, so support can correlate it with server logs.
PATH C — PageExpiredException (lines 139-143): The user's Wicket session timed out. showFeedback = false — no feedback form, because the user didn't encounter a bug, they just waited too long. The title is set to "Message" (line 143) instead of the default "Error".
PATH D — Everything else (lines 144-148): Unknown exceptions (NullPointerException, Hibernate exceptions, etc.). Same treatment as ServletException — message number + "contact dev team".
private void sendProactiveMessageToSupportTeam() {
if (configService.getPfSupportMailAddress() != null && configService.isSendMailConfigured()) {
SendFeedbackData errorData = new SendFeedbackData();
errorData.setSender(sendMail.getMailFromStandardEmailSender());
errorData.setReceiver(configService.getPfSupportMailAddress());
errorData.setSubject("Error occurred: #" + messageNumber + " on " + domainService.getDomain());
errorData.setDescription(
"Error occurred at: " + dateString + "(" + timeZone + ")" +
" with number: #" + messageNumber +
" from user: " + sender +
" Exception stack trace: \n" + rootCauseStackTrace);
sendFeedback.send(errorData);
}
}This is the proactive monitoring system. When a critical error occurs AND the support email is configured, the system automatically sends an email containing:
This is essentially a built-in error monitoring system without needing Sentry, Datadog, or ELK. For self-hosted deployments, this is the primary way administrators learn about production errors. The email goes to configService.getPfSupportMailAddress() — configured in projectforge.properties.
When showFeedback = true AND a message number was generated AND a receiver email is configured, the page renders an ErrorForm (Wicket form component) that lets users submit error reports. Pre-filled data:
| Field | Value |
|---|---|
| Receiver | From ConfigurationParam.FEEDBACK_E_MAIL |
| Message number | The System.currentTimeMillis() ID |
| Message | throwable.getMessage() — the exception's short message |
| Stack trace | Full stack trace via ExceptionHelper.printStackTrace() |
| Sender | Current logged-in user's full name |
| Subject | "ProjectForge-Error #" + messageNumber + " from " + sender |
| Root cause | Root cause message + its stack trace |
The sendFeedback() method (lines 229-246) handles the submit: calls SendFeedback.send(), redirects to MessagePage with success/failure message. On failure, shows "mail.error.exception" with setWarning(true).
if (throwable == null ||
throwable instanceof ComponentNotFoundException ||
throwable instanceof ConnectException) {
// SILENT — these aren't bugs, they're expected lifecycle events
} else if (rootCause instanceof UserException) {
// SILENT — already presented to the user
} else if (GlobalExceptionRegistry.sendMailToDevelopers(throwable)) {
sendProactiveMessageToSupportTeam(); // Only send email for real bugs
}Three categories of exceptions are deliberately suppressed — no email is sent, no proactive notification:
| Exception | Why suppressed |
|---|---|
null throwable | ErrorPage called without an exception — from CallAllPagesTest (line 188-189 comment). The test suite navigates to ErrorPage directly to verify it renders. |
ComponentNotFoundException | Normal Wicket lifecycle. When a user navigates through the AddressListPage, image components are removed by the framework — Wicket throws this when trying to find a component that was already cleaned up. Not a bug. |
ConnectException | The user's browser disconnected. Happens when the user clicks away during a long-running request — by the time the response is ready, the browser is gone. Not a bug. |
GlobalExceptionRegistry static block (lines 67-70): Pre-registers StalePageException and ComponentNotFoundException with human-readable descriptions ("Wicket page not available anymore"). The sendMailToDevelopers() check on line 196 consults this registry — only exceptions NOT registered as "known/expected" trigger proactive emails.
@Override public boolean isVersioned() { return false; } // Never store page versions
@Override public boolean isErrorPage() { return true; } // Tell Wicket this IS an error pageisVersioned() = false (line 257): Wicket normally versions pages — stores old copies so the browser back button works. For error pages, versioning is disabled because: (1) error pages are transient — you shouldn't navigate back to an error, (2) versioning stores the exception object in the session, which could leak memory or contain sensitive data, (3) error pages should always show fresh content on revisit.
isErrorPage() = true (line 265): Tells Wicket's exception handling chain that THIS page is the error page — prevents infinite recursion if the error page itself throws an exception (Wicket would then show its own fallback error page).
The Pac-Man Easter egg (#3780) is linked FROM this error page. The HTML template (ErrorPage.html, 24 lines) shows the structure: an alert-danger div for the error message, a FeedbackPanel for user reports, and a form with action buttons. The "Play Pac-Man" button would be rendered as one of the form's action buttons — turning a 500 error into a game invitation. This is the moment where frustration becomes fun.
Three ProjectForge-specific exception types, each handled with i18n translation. The key difference between them:
UserExceptiongetMsgParams()— user-facing paramsInternalErrorExceptiongetMsgParams()— internal paramsAccessExceptiongetParams()/getMessageArgs()— access paramsThe UnsupportedOperationException on line 115 is a deliberate crash: If an unknown
ProjectForgeExceptionsubclass reaches this point, the developer forgot to add a handler. The comment "For developer: Please add unknown ProjectForgeException here!" is a self-documenting design — the crash forces the developer to handle new exception types explicitly, rather than silently falling through with a generic error message.StackTrace filtering:
ONLY4NAMESPACE = "org.projectforge"(line 65) limits stack traces to ProjectForge packages only. This keeps logs clean — no dozens of Spring/Hibernate/Tomcat frames cluttering error reports.ExceptionHelper.getFilteredStackTrace(ex, ONLY4NAMESPACE)strips everything outsideorg.projectforge.