diff --git a/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java
index 8019ff4335..2a2f631094 100644
--- a/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java
+++ b/openam-core/src/main/java/org/openidentityplatform/openam/click/ClickServlet.java
@@ -1933,9 +1933,8 @@ protected boolean processAjaxTargetControls(Context context,
* which allows the JavaScript that initiated the Ajax request to handle
* the error as appropriate.
*
- * If Click is running in development modes the exception stackTrace
- * will be rendered, in production modes an error message is
- * rendered.
+ * A fixed error message is rendered whatever the application mode; the
+ * exception itself is logged.
*
* Below is an example error response:
*
@@ -1968,11 +1967,9 @@ protected void handleAjaxException(HttpServletRequest request,
// TODO: use an ErrorReport instance instead?
writer.write("
\n");
- if (configService.isProductionMode() || configService.isProfileMode()) {
- writer.write("The application encountered an unexpected error.");
- } else {
- exception.printStackTrace(writer);
- }
+ // The exception is logged below; whatever the application mode, neither
+ // its stack trace nor its message is for the browser.
+ writer.write("The application encountered an unexpected error.");
writer.write("\n
");
} finally {
diff --git a/openam-core/src/test/java/org/openidentityplatform/openam/click/ClickServletAjaxErrorTest.java b/openam-core/src/test/java/org/openidentityplatform/openam/click/ClickServletAjaxErrorTest.java
new file mode 100644
index 0000000000..f975680a7d
--- /dev/null
+++ b/openam-core/src/test/java/org/openidentityplatform/openam/click/ClickServletAjaxErrorTest.java
@@ -0,0 +1,82 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.openidentityplatform.openam.click;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.openidentityplatform.openam.click.service.ConfigService;
+import org.openidentityplatform.openam.click.service.LogService;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * An Ajax request that fails answers a fixed message: the exception, its message included,
+ * belongs in the log, whatever the application mode.
+ */
+public class ClickServletAjaxErrorTest {
+
+ private static final String SECRET = "LDAP bind as cn=Directory Manager failed: ";
+
+ /** The servlet with the response writer and the services it reaches for replaced. */
+ private static final class Fixture {
+ final StringWriter body = new StringWriter();
+ final LogService log = mock(LogService.class);
+ final ClickServlet servlet = new ClickServlet() {
+ @Override
+ PrintWriter getPrintWriter(HttpServletResponse response) {
+ return new PrintWriter(body);
+ }
+ };
+
+ Fixture(boolean productionMode) {
+ ConfigService config = mock(ConfigService.class);
+ when(config.isProductionMode()).thenReturn(productionMode);
+ when(config.isProfileMode()).thenReturn(false);
+ servlet.configService = config;
+ servlet.logger = log;
+ }
+ }
+
+ @DataProvider
+ public Object[][] modes() {
+ return new Object[][] {{true}, {false}};
+ }
+
+ @Test(dataProvider = "modes")
+ public void theResponseCarriesAFixedMessageAndNotTheException(boolean productionMode) {
+ Fixture fixture = new Fixture(productionMode);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ IllegalStateException failure = new IllegalStateException(SECRET);
+
+ fixture.servlet.handleAjaxException(mock(HttpServletRequest.class), response, false, failure, null);
+
+ String body = fixture.body.toString();
+ assertThat(body).contains("The application encountered an unexpected error.");
+ assertThat(body).doesNotContain("IllegalStateException").doesNotContain("Directory Manager")
+ .doesNotContain("