#3202: ErrorPage.java

projectforge-wicket/src/main/java/org/projectforge/web/wicket/ErrorPage.java

Path: projectforge-wicket/src/main/java/org/projectforge/web/wicket/ErrorPage.java · Lines: 268 · Extends: AbstractSecuredPage · Author: Kai Reinhard

Purpose: THE global error page — catches EVERY unhandled exception in the Wicket webapp. Classifies exceptions into 6 categories, logs with unique message numbers (System.currentTimeMillis()), shows user-friendly translated messages, optionally renders a feedback form for users to submit error reports, and proactively emails the support team for critical errors. This is where the Pac-Man Easter egg is linked.

Source: GitHub

268 lines · 199 code · 48 comments · 21 blank
CommitMessage
9ebb885222016-07-18 Initial commit

Original code from Kai Reinhard. Has survived the Wicket→React migration — still active in the Wicket module.

The 6-way exception classifier — lines 88-116

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);
}

Three ProjectForge-specific exception types, each handled with i18n translation. The key difference between them:

ExceptionUser seesLog levelParams
UserExceptionTranslated message from i18n key + message parametersINFO (if doLog)getMsgParams() — user-facing params
InternalErrorExceptionSame as UserException — translated messageINFO (if doLog)getMsgParams() — internal params
AccessExceptionLocalized "access denied" with paramsINFO (if doLog)getParams() / getMessageArgs() — access params

The UnsupportedOperationException on line 115 is a deliberate crash: If an unknown ProjectForgeException subclass 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 outside org.projectforge.

The constructor — 4 exception paths, 81 lines of error routing (lines 118-199)

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 messageNumberSystem.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".

Proactive error reporting — email the support team (lines 201-223)

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.

User feedback form — crowd-sourced bug reports (lines 150-178)

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:

FieldValue
ReceiverFrom ConfigurationParam.FEEDBACK_E_MAIL
Message numberThe System.currentTimeMillis() ID
Messagethrowable.getMessage() — the exception's short message
Stack traceFull stack trace via ExceptionHelper.printStackTrace()
SenderCurrent logged-in user's full name
Subject"ProjectForge-Error #" + messageNumber + " from " + sender
Root causeRoot 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).

Exception suppression — when to stay silent (lines 180-198)

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:

ExceptionWhy suppressed
null throwableErrorPage called without an exception — from CallAllPagesTest (line 188-189 comment). The test suite navigates to ErrorPage directly to verify it renders.
ComponentNotFoundExceptionNormal 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.
ConnectExceptionThe 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.

Wicket overrides — page identity

@Override public boolean isVersioned() { return false; }    // Never store page versions
@Override public boolean isErrorPage() { return true; }     // Tell Wicket this IS an error page

isVersioned() = 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).

Where Pac-Man fits in

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.