Skip to content

WW-5690 perf(dispatcher): load the dev-mode error template on first use - #1864

Merged
lukaszlenart merged 2 commits into
mainfrom
fix/WW-5690-lazy-error-template
Aug 24, 2026
Merged

WW-5690 perf(dispatcher): load the dev-mode error template on first use#1864
lukaszlenart merged 2 commits into
mainfrom
fix/WW-5690-lazy-error-template

Conversation

@lukaszlenart

@lukaszlenart lukaszlenart commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes WW-5690

DefaultDispatcherErrorHandler.init() built a FreeMarker configuration and loaded /org/apache/struts2/dispatcher/error.ftl on every startup — including production, where the problem report is never rendered. handleError() only reaches it when devMode is on; otherwise it delegates to the container's error page.

So every Struts application today pays to initialise FreeMarker for a page most of them never show.

Change

init() now just stores the ServletContext. A new protected synchronized getTemplate() loads the template on first use and caches it.

The lock is deliberate rather than lock-free: this path only runs when devMode is on and a request has already failed, so it is never hot, and FreemarkerManager.getConfiguration is itself synchronized — the path was already taking a lock. A plain field behind a synchronized getter is simpler to verify than a volatile one, and avoids java:S3077 (volatile publishes the reference, not the object's state, and Template extends Configurable).

One behavioural change

A missing or unparsable error.ftl used to throw StrutsException from init() and fail the application at boot. It now surfaces on the first dev-mode error, where the existing catch (Exception exp) in handleErrorInDevMode() already degrades to sendError(code, "Unable to show problem report: ...").

I'd argue that's an improvement — a dev-only template shouldn't stop a production application starting — but it is a change in failure timing and worth a reviewer's eye.

Scope

Startup cost only. The FreemarkerManager injection, the freemarker.template.Template import and error.ftl itself all stay — removing them is WW-5693, targeted at 8.0.0 because it deletes a template that can be overridden on the classpath.

Both are part of the lean-core work tracked in WW-5689, alongside WW-5691 and WW-5692 for the other two places core reaches into views.freemarker.

Tests

Two added to DefaultDispatcherErrorHandlerTest, using a FreemarkerManager subclass that records whether getConfiguration was called (no mocking framework needed — it delegates to super, so the template really renders):

  • testInitDoesNotLoadErrorTemplate — asserts init() leaves FreeMarker untouched. Verified failing before the change, on the assertion, which is what makes it a real regression guard.
  • testErrorTemplateLoadedOnFirstDevModeError — asserts the deferred load actually happens, so the report can't silently stop rendering.

The four existing tests are unchanged and still pass. Full core suite: 3197 tests, 0 failures, 0 errors.

🤖 Generated with Claude Code

lukaszlenart and others added 2 commits August 24, 2026 11:56
DefaultDispatcherErrorHandler.init() built a FreeMarker configuration and
loaded /org/apache/struts2/dispatcher/error.ftl on every startup, including
in production where the problem report is never rendered - handleError()
delegates to the container's error page unless devMode is on.

Defer the load to the first dev-mode error instead. Two threads racing there
may both load the template, which is harmless: FreeMarker caches templates in
its own configuration, and that is cheaper than locking a path taken once per
application.

One behavioural change: a missing or unparsable error.ftl used to fail the
application at boot. It now surfaces on the first dev-mode error, where the
existing catch in handleErrorInDevMode() degrades to sendError() with the
cause. A dev-only template should not stop a production application starting.

This removes the startup cost only; the FreeMarker dependency itself stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sonar flags the volatile Template field (java:S3077): volatile publishes the
reference safely but guarantees nothing about the object's own state, and
Template extends Configurable, so it is not strictly immutable.

The lock-free version was not worth defending anyway. It was justified as
avoiding a lock on a path taken once per application, but that path only runs
when devMode is on and a request has already failed - it is never hot. And
FreemarkerManager.getConfiguration is itself synchronized, so this path was
already taking a lock.

A plain field behind a synchronized getter is simpler, obviously correct, and
costs nothing here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 05ad78a into main Aug 24, 2026
12 of 13 checks passed
@lukaszlenart
lukaszlenart deleted the fix/WW-5690-lazy-error-template branch August 24, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant