You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by st...@apache.org on 2023/09/12 20:51:25 UTC

[solr] branch main updated: SOLR-14886 : suppress stack traces in query response (#1632)

This is an automated email from the ASF dual-hosted git repository.

stillalex pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new cf302655ef6 SOLR-14886 : suppress stack traces in query response (#1632)
cf302655ef6 is described below

commit cf302655ef6406c158f1866b4de9f56d5ca7dde8
Author: igiguere <ig...@users.noreply.github.com>
AuthorDate: Tue Sep 12 16:51:19 2023 -0400

    SOLR-14886 : suppress stack traces in query response (#1632)
    
    Added a property "hideStackTrace" to solr.xml or system property 'solr.hideStackTrace' to not include error stack trace in the response.
    ---------
    Co-authored-by: Christine Poerschke <cp...@apache.org>
    Co-authored-by: Alex Deparvu <st...@apache.org>
---
 solr/CHANGES.txt                                   |   1 +
 .../java/org/apache/solr/core/CoreContainer.java   |   4 +
 .../src/java/org/apache/solr/core/NodeConfig.java  |  15 ++
 .../java/org/apache/solr/core/SolrXmlConfig.java   |   3 +
 .../solr/handler/component/QueryComponent.java     |  16 +-
 .../solr/handler/component/SearchHandler.java      |   8 +-
 .../solr/jersey/CatchAllExceptionMapper.java       |   8 +-
 .../org/apache/solr/rest/BaseSolrResource.java     |   7 +-
 .../SearchGroupShardResponseProcessor.java         |   8 +-
 .../TopGroupsShardResponseProcessor.java           |   8 +-
 .../java/org/apache/solr/servlet/HttpSolrCall.java |  14 +-
 .../org/apache/solr/servlet/ResponseUtils.java     |  62 +++++++-
 solr/core/src/test-files/solr/solr-50-all.xml      |   1 +
 solr/core/src/test-files/solr/solr.xml             |   1 +
 .../src/test/org/apache/solr/core/TestSolrXml.java |   1 +
 .../apache/solr/servlet/HideStackTraceTest.java    | 175 +++++++++++++++++++++
 solr/server/solr/solr.xml                          |   1 +
 .../pages/configuring-solr-xml.adoc                |  10 ++
 18 files changed, 317 insertions(+), 26 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 41c21f7f90b..aed23219f66 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -123,6 +123,7 @@ Improvements
 
 * SOLR-16970: SOLR_OPTS is now able to override options set by the Solr control scripts, "bin/solr" and "bin/solr.cmd". (Houston Putman)
 
+* SOLR-14886: Suppress stack traces in query response (Isabelle Giguere via Alex Deparvu)
 
 Optimizations
 ---------------------
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 0bc9ad59d27..b35cbbef058 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -2443,6 +2443,10 @@ public class CoreContainer {
     return status;
   }
 
+  public boolean hideStackTrace() {
+    return cfg.hideStackTraces();
+  }
+
   /**
    * Retrieve the aliases from zookeeper. This is typically cached and does not hit zookeeper after
    * the first use.
diff --git a/solr/core/src/java/org/apache/solr/core/NodeConfig.java b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
index 581a2d07c0a..9177889f447 100644
--- a/solr/core/src/java/org/apache/solr/core/NodeConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
@@ -67,6 +67,8 @@ public class NodeConfig {
 
   private final List<String> allowUrls;
 
+  private final boolean hideStackTraces;
+
   private final String sharedLibDirectory;
 
   private final String modules;
@@ -153,6 +155,7 @@ public class NodeConfig {
       String defaultZkHost,
       Set<Path> allowPaths,
       List<String> allowUrls,
+      boolean hideStackTraces,
       String configSetServiceClass,
       String modules,
       Set<String> hiddenSysProps) {
@@ -189,6 +192,7 @@ public class NodeConfig {
     this.defaultZkHost = defaultZkHost;
     this.allowPaths = allowPaths;
     this.allowUrls = allowUrls;
+    this.hideStackTraces = hideStackTraces;
     this.configSetServiceClass = configSetServiceClass;
     this.modules = modules;
     this.hiddenSysProps = hiddenSysProps;
@@ -452,6 +456,10 @@ public class NodeConfig {
     return allowUrls;
   }
 
+  public boolean hideStackTraces() {
+    return hideStackTraces;
+  }
+
   // Configures SOLR_HOME/lib to the shared class loader
   private void setupSharedLib() {
     // Always add $SOLR_HOME/lib to the shared resource loader
@@ -615,6 +623,7 @@ public class NodeConfig {
     private String defaultZkHost;
     private Set<Path> allowPaths = Collections.emptySet();
     private List<String> allowUrls = Collections.emptyList();
+    private boolean hideStackTrace = Boolean.getBoolean("solr.hideStackTrace");
 
     private final Path solrHome;
     private final String nodeName;
@@ -809,6 +818,11 @@ public class NodeConfig {
       return this;
     }
 
+    public NodeConfigBuilder setHideStackTrace(boolean hide) {
+      this.hideStackTrace = hide;
+      return this;
+    }
+
     public NodeConfigBuilder setConfigSetServiceClass(String configSetServiceClass) {
       this.configSetServiceClass = configSetServiceClass;
       return this;
@@ -902,6 +916,7 @@ public class NodeConfig {
           defaultZkHost,
           allowPaths,
           allowUrls,
+          hideStackTrace,
           configSetServiceClass,
           modules,
           resolveHiddenSysPropsFromSysPropOrEnvOrDefault(hiddenSysProps));
diff --git a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
index 85fbaeeb193..df0645c8f69 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
@@ -372,6 +372,9 @@ public class SolrXmlConfig {
               case "allowPaths":
                 builder.setAllowPaths(separatePaths(it.txt()));
                 break;
+              case "hideStackTrace":
+                builder.setHideStackTrace(it.boolVal(false));
+                break;
               case "configSetBaseDir":
                 builder.setConfigSetBaseDirectory(it.txt());
                 break;
diff --git a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index 5efde4786ce..a2b46a30d46 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -906,9 +906,11 @@ public class QueryComponent extends SearchComponent {
             t = ((SolrServerException) t).getCause();
           }
           nl.add("error", t.toString());
-          StringWriter trace = new StringWriter();
-          t.printStackTrace(new PrintWriter(trace));
-          nl.add("trace", trace.toString());
+          if (!rb.req.getCore().getCoreContainer().hideStackTrace()) {
+            StringWriter trace = new StringWriter();
+            t.printStackTrace(new PrintWriter(trace));
+            nl.add("trace", trace.toString());
+          }
           if (srsp.getShardAddress() != null) {
             nl.add("shardAddress", srsp.getShardAddress());
           }
@@ -1297,9 +1299,11 @@ public class QueryComponent extends SearchComponent {
                 t = ((SolrServerException) t).getCause();
               }
               nl.add("error", t.toString());
-              StringWriter trace = new StringWriter();
-              t.printStackTrace(new PrintWriter(trace));
-              nl.add("trace", trace.toString());
+              if (!rb.req.getCore().getCoreContainer().hideStackTrace()) {
+                StringWriter trace = new StringWriter();
+                t.printStackTrace(new PrintWriter(trace));
+                nl.add("trace", trace.toString());
+              }
             }
           }
 
diff --git a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
index 10f99ac49f8..be35ad99a1d 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
@@ -601,9 +601,11 @@ public class SearchHandler extends RequestHandlerBase
           }
         }
         nl.add("error", cause.toString());
-        StringWriter trace = new StringWriter();
-        cause.printStackTrace(new PrintWriter(trace));
-        nl.add("trace", trace.toString());
+        if (!core.getCoreContainer().hideStackTrace()) {
+          StringWriter trace = new StringWriter();
+          cause.printStackTrace(new PrintWriter(trace));
+          nl.add("trace", trace.toString());
+        }
       } else if (rb.getResults() != null) {
         nl.add("numFound", rb.getResults().docList.matches());
         nl.add(
diff --git a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
index d3886ae8727..12e3b5ef017 100644
--- a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
@@ -108,8 +108,12 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
         containerRequestContext.getProperty(SOLR_JERSEY_RESPONSE) == null
             ? new SolrJerseyResponse()
             : (SolrJerseyResponse) containerRequestContext.getProperty(SOLR_JERSEY_RESPONSE);
-
-    response.error = ResponseUtils.getTypedErrorInfo(normalizedException, log);
+    response.error =
+        ResponseUtils.getTypedErrorInfo(
+            normalizedException,
+            log,
+            solrQueryRequest.getCore() != null
+                && solrQueryRequest.getCore().getCoreContainer().hideStackTrace());
     response.responseHeader.status = response.error.code;
     final String mediaType =
         V2ApiUtils.getMediaTypeFromWtParam(solrQueryRequest, MediaType.APPLICATION_JSON);
diff --git a/solr/core/src/java/org/apache/solr/rest/BaseSolrResource.java b/solr/core/src/java/org/apache/solr/rest/BaseSolrResource.java
index 2e63d5cab2e..cbfa72993b7 100644
--- a/solr/core/src/java/org/apache/solr/rest/BaseSolrResource.java
+++ b/solr/core/src/java/org/apache/solr/rest/BaseSolrResource.java
@@ -143,7 +143,12 @@ public abstract class BaseSolrResource {
     Exception exception = getSolrResponse().getException();
     if (null != exception) {
       NamedList<Object> info = new SimpleOrderedMap<>();
-      this.statusCode = ResponseUtils.getErrorInfo(exception, info, log);
+      this.statusCode =
+          ResponseUtils.getErrorInfo(
+              exception,
+              info,
+              log,
+              solrCore != null && solrCore.getCoreContainer().hideStackTrace());
       getSolrResponse().add("error", info);
       String message = (String) info.get("msg");
       if (null != message && !message.trim().isEmpty()) {
diff --git a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
index a748f917977..a88817de8ff 100644
--- a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
+++ b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
@@ -89,9 +89,11 @@ public class SearchGroupShardResponseProcessor implements ShardResponseProcessor
             t = t.getCause();
           }
           nl.add("error", t.toString());
-          StringWriter trace = new StringWriter();
-          t.printStackTrace(new PrintWriter(trace));
-          nl.add("trace", trace.toString());
+          if (!rb.req.getCore().getCoreContainer().hideStackTrace()) {
+            StringWriter trace = new StringWriter();
+            t.printStackTrace(new PrintWriter(trace));
+            nl.add("trace", trace.toString());
+          }
         } else {
           nl.add("numFound", response.get("totalHitCount"));
         }
diff --git a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
index 39060f60782..4c7b46c7e77 100644
--- a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
+++ b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
@@ -101,9 +101,11 @@ public class TopGroupsShardResponseProcessor implements ShardResponseProcessor {
             t = ((SolrServerException) t).getCause();
           }
           individualShardInfo.add("error", t.toString());
-          StringWriter trace = new StringWriter();
-          t.printStackTrace(new PrintWriter(trace));
-          individualShardInfo.add("trace", trace.toString());
+          if (!rb.req.getCore().getCoreContainer().hideStackTrace()) {
+            StringWriter trace = new StringWriter();
+            t.printStackTrace(new PrintWriter(trace));
+            individualShardInfo.add("trace", trace.toString());
+          }
         } else {
           // summary for successful shard response is added down below
         }
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
index f1e5a25693c..9137f3ded4e 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -837,7 +837,12 @@ public class HttpSolrCall {
       try {
         if (exp != null) {
           SimpleOrderedMap<Object> info = new SimpleOrderedMap<>();
-          int code = ResponseUtils.getErrorInfo(ex, info, log);
+          int code =
+              ResponseUtils.getErrorInfo(
+                  ex,
+                  info,
+                  log,
+                  localCore != null && localCore.getCoreContainer().hideStackTrace());
           sendError(code, info.toString());
         }
       } finally {
@@ -971,7 +976,12 @@ public class HttpSolrCall {
 
       if (solrRsp.getException() != null) {
         NamedList<Object> info = new SimpleOrderedMap<>();
-        int code = ResponseUtils.getErrorInfo(solrRsp.getException(), info, log);
+        int code =
+            ResponseUtils.getErrorInfo(
+                solrRsp.getException(),
+                info,
+                log,
+                core != null && core.getCoreContainer().hideStackTrace());
         solrRsp.add("error", info);
         response.setStatus(code);
       }
diff --git a/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java b/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
index 337243e25e6..1f7427f669f 100644
--- a/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
+++ b/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
@@ -28,6 +28,10 @@ import org.slf4j.Logger;
 public class ResponseUtils {
   private ResponseUtils() {}
 
+  // System property to use if the Solr core does not exist or solr.hideStackTrace is not
+  // configured. (i.e.: a lot of unit test).
+  private static final boolean SYSTEM_HIDE_STACK_TRACES = Boolean.getBoolean("solr.hideStackTrace");
+
   /**
    * Adds the given Throwable's message to the given NamedList.
    *
@@ -42,6 +46,27 @@ public class ResponseUtils {
    * @see #getTypedErrorInfo(Throwable, Logger)
    */
   public static int getErrorInfo(Throwable ex, NamedList<Object> info, Logger log) {
+    return getErrorInfo(ex, info, log, false);
+  }
+
+  /**
+   * Adds the given Throwable's message to the given NamedList.
+   *
+   * <p>Primarily used by v1 code; v2 endpoints or dispatch code should call {@link
+   * #getTypedErrorInfo(Throwable, Logger)}
+   *
+   * <p>If the response code is not a regular code, the Throwable's stack trace is both logged and
+   * added to the given NamedList.
+   *
+   * <p>Status codes less than 100 are adjusted to be 500.
+   *
+   * <p>Stack trace will not be output if hideTrace=true OR system property
+   * solr.hideStackTrace=true.
+   *
+   * @see #getTypedErrorInfo(Throwable, Logger)
+   */
+  public static int getErrorInfo(
+      Throwable ex, NamedList<Object> info, Logger log, boolean hideTrace) {
     int code = 500;
     if (ex instanceof SolrException) {
       SolrException solrExc = (SolrException) ex;
@@ -70,10 +95,13 @@ public class ResponseUtils {
 
     // For any regular code, don't include the stack trace
     if (code == 500 || code < 100) {
-      StringWriter sw = new StringWriter();
-      ex.printStackTrace(new PrintWriter(sw));
+      // hide all stack traces, as configured
+      if (!hideStackTrace(hideTrace)) {
+        StringWriter sw = new StringWriter();
+        ex.printStackTrace(new PrintWriter(sw));
+        info.add("trace", sw.toString());
+      }
       log.error("500 Exception", ex);
-      info.add("trace", sw.toString());
 
       // non standard codes have undefined results with various servers
       if (code < 100) {
@@ -96,6 +124,22 @@ public class ResponseUtils {
    * @see #getErrorInfo(Throwable, NamedList, Logger)
    */
   public static ErrorInfo getTypedErrorInfo(Throwable ex, Logger log) {
+    return getTypedErrorInfo(ex, log, false);
+  }
+
+  /**
+   * Adds information about the given Throwable to a returned {@link ErrorInfo}
+   *
+   * <p>Primarily used by v2 API code, which can handle such typed information.
+   *
+   * <p>Status codes less than 100 are adjusted to be 500.
+   *
+   * <p>Stack trace will not be output if hideTrace=true OR system property
+   * solr.hideStackTrace=true.
+   *
+   * @see #getErrorInfo(Throwable, NamedList, Logger)
+   */
+  public static ErrorInfo getTypedErrorInfo(Throwable ex, Logger log, boolean hideTrace) {
     final ErrorInfo errorInfo = new ErrorInfo();
     int code = 500;
     if (ex instanceof SolrException) {
@@ -120,10 +164,12 @@ public class ResponseUtils {
 
     // For any regular code, don't include the stack trace
     if (code == 500 || code < 100) {
-      StringWriter sw = new StringWriter();
-      ex.printStackTrace(new PrintWriter(sw));
+      if (!hideStackTrace(hideTrace)) {
+        StringWriter sw = new StringWriter();
+        ex.printStackTrace(new PrintWriter(sw));
+        errorInfo.trace = sw.toString();
+      }
       log.error("500 Exception", ex);
-      errorInfo.trace = sw.toString();
 
       // non standard codes have undefined results with various servers
       if (code < 100) {
@@ -135,4 +181,8 @@ public class ResponseUtils {
     errorInfo.code = code;
     return errorInfo;
   }
+
+  private static boolean hideStackTrace(final boolean hideTrace) {
+    return hideTrace || SYSTEM_HIDE_STACK_TRACES;
+  }
 }
diff --git a/solr/core/src/test-files/solr/solr-50-all.xml b/solr/core/src/test-files/solr/solr-50-all.xml
index 6803d2be0db..4680027ecd9 100644
--- a/solr/core/src/test-files/solr/solr-50-all.xml
+++ b/solr/core/src/test-files/solr/solr-50-all.xml
@@ -29,6 +29,7 @@
   <int name="transientCacheSize">66</int>
   <int name="replayUpdatesThreads">100</int>
   <int name="maxBooleanClauses">42</int>
+  <bool name="hideStackTrace">true</bool>
 
   <coreAdminHandlerActions>
     <str name="action1">testCoreAdminHandlerAction1</str>
diff --git a/solr/core/src/test-files/solr/solr.xml b/solr/core/src/test-files/solr/solr.xml
index c7c6b44a362..b2606a44c6d 100644
--- a/solr/core/src/test-files/solr/solr.xml
+++ b/solr/core/src/test-files/solr/solr.xml
@@ -28,6 +28,7 @@
   <str name="coreRootDirectory">${coreRootDirectory:.}</str>
   <str name="allowPaths">${solr.allowPaths:}</str>
   <str name="allowUrls">${solr.tests.allowUrls:}</str>
+  <bool name="hideStackTrace">${solr.hideStackTrace:true}</bool>
 
   <shardHandlerFactory name="shardHandlerFactory" class="HttpShardHandlerFactory">
     <str name="urlScheme">${urlScheme:}</str>
diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
index 51ac2bb0e65..3445db478e4 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
@@ -135,6 +135,7 @@ public class TestSolrXml extends SolrTestCaseJ4 {
                     : Set.of("/tmp", "/home/john").stream()
                         .map(s -> Path.of(s))
                         .collect(Collectors.toSet())));
+    assertTrue("hideStackTrace", cfg.hideStackTraces());
     System.clearProperty("solr.allowPaths");
   }
 
diff --git a/solr/core/src/test/org/apache/solr/servlet/HideStackTraceTest.java b/solr/core/src/test/org/apache/solr/servlet/HideStackTraceTest.java
new file mode 100644
index 00000000000..dc6af6f2716
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/servlet/HideStackTraceTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.servlet;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.util.EntityUtils;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.handler.component.ResponseBuilder;
+import org.apache.solr.handler.component.SearchComponent;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/** SOLR-14886 : Suppress stack trace in Query response */
+@SuppressSSL
+public class HideStackTraceTest extends SolrTestCaseJ4 {
+
+  @ClassRule public static final SolrJettyTestRule solrRule = new SolrJettyTestRule();
+
+  @BeforeClass
+  public static void setupSolrHome() throws Exception {
+
+    System.setProperty("solr.hideStackTrace", "true");
+
+    Path configSet = createTempDir("configSet");
+    copyMinConf(configSet.toFile());
+    // insert a special filterCache configuration
+    Path solrConfig = configSet.resolve("conf/solrconfig.xml");
+    Files.writeString(
+        solrConfig,
+        Files.readString(solrConfig)
+            .replace(
+                "</config>",
+                "  <searchComponent name=\"errorComponent\" class=\"org.apache.solr.servlet.HideStackTraceTest$ErrorComponent\"/>\n"
+                    + "  <requestHandler name=\"/withError\" class=\"solr.SearchHandler\">\n"
+                    + "    <arr name=\"first-components\">\n"
+                    + "      <str>errorComponent</str>\n"
+                    + "    </arr>\n"
+                    + "  </requestHandler>\n"
+                    + "</config>"));
+
+    solrRule.startSolr(LuceneTestCase.createTempDir());
+    solrRule.newCollection().withConfigSet(configSet.toString()).create();
+  }
+
+  @AfterClass
+  public static void cleanup() throws Exception {
+    System.clearProperty("solr.hideStackTrace");
+  }
+
+  @Test
+  public void testHideStackTrace() throws Exception {
+    // Normal stack:
+    // {
+    // "responseHeader":{
+    // "status":500,
+    // "QTime":10
+    // },
+    // "error":{
+    // "msg":"Stack trace should not be populated.",
+    // "trace":"java.lang.RuntimeException: Stack trace should not be populated.\n\tat
+    // org.apache.solr.servlet.HideStackTraceTest$ErrorComponent.process(HideStackTraceTest.java:130)\n\tat
+    // org.apache.solr.handler.component.SearchHandler.handleRequestBody(SearchHandler.java:442)\n\tat
+    // org.apache.solr.handler.RequestHandlerBase.handleRequest(RequestHandlerBase.java:224)\n\tat
+    // org.apache.solr.core.SolrCore.execute(SolrCore.java:2890)\n\tat
+    // org.apache.solr.servlet.HttpSolrCall.executeCoreRequest(HttpSolrCall.java:870)\n\tat
+    // org.apache.solr.servlet.HttpSolrCall.call(HttpSolrCall.java:559)\n\tat
+    // org.apache.solr.servlet.SolrDispatchFilter.dispatch(SolrDispatchFilter.java:254)\n\tat
+    // org.apache.solr.servlet.SolrDispatchFilter.lambda$0(SolrDispatchFilter.java:215)\n\tat
+    // org.apache.solr.servlet.ServletUtils.traceHttpRequestExecution2(ServletUtils.java:241)\n\tat
+    // org.apache.solr.servlet.ServletUtils.rateLimitRequest(ServletUtils.java:211)\n\tat
+    // org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:209)\n\tat
+    // org.apache.solr.servlet.SolrDispatchFilter.doFilter(SolrDispatchFilter.java:192)\n\tat
+    // org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)\n\tat
+    // org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1635)\n\tat
+    // org.apache.solr.embedded.JettySolrRunner$DebugFilter.doFilter(JettySolrRunner.java:187)\n\tat
+    // org.eclipse.jetty.servlet.FilterHolder.doFilter(FilterHolder.java:202)\n\tat
+    // org.eclipse.jetty.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1635)\n\tat
+    // org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:527)\n\tat
+    // org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)\n\tat
+    // org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1570)\n\tat
+    // org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)\n\tat
+    // org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1384)\n\tat
+    // org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)\n\tat
+    // org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)\n\tat
+    // org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1543)\n\tat
+    // org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)\n\tat
+    // org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1306)\n\tat
+    // org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)\n\tat
+    // org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)\n\tat
+    // org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:301)\n\tat
+    // org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)\n\tat
+    // org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:822)\n\tat
+    // org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)\n\tat
+    // org.eclipse.jetty.server.Server.handle(Server.java:563)\n\tat
+    // org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:505)\n\tat
+    // org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:762)\n\tat
+    // org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:497)\n\tat
+    // org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)\n\tat
+    // org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)\n\tat
+    // org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)\n\tat
+    // org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)\n\tat
+    // org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)\n\tat
+    // org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)\n\tat
+    // org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)\n\tat
+    // java.base/java.lang.Thread.run(Thread.java:833)\n",
+    // "code":500
+    // }
+    // }
+
+    // Supressed stack:
+    // {
+    // "responseHeader":{
+    // "status":500,
+    // "QTime":9
+    // },
+    // "error":{
+    // "msg":"Stack trace should not be populated.",
+    // "code":500
+    // }
+    // }
+
+    final String url = solrRule.getBaseUrl().toString() + "/collection1/withError?q=*:*&wt=json";
+    final HttpGet get = new HttpGet(url);
+    try (var client = HttpClientUtil.createClient(null);
+        CloseableHttpResponse response = client.execute(get)) {
+      assertEquals(500, response.getStatusLine().getStatusCode());
+      String responseJson = EntityUtils.toString(response.getEntity());
+      assertFalse(responseJson.contains("\"trace\""));
+      assertFalse(
+          responseJson.contains("org.apache.solr.servlet.HideStackTraceTest$ErrorComponent"));
+    }
+  }
+
+  public static class ErrorComponent extends SearchComponent {
+
+    @Override
+    public void prepare(ResponseBuilder rb) throws IOException {
+      // prepared
+    }
+
+    @Override
+    public void process(ResponseBuilder rb) throws IOException {
+      throw new RuntimeException("Stack trace should not be populated.");
+    }
+
+    @Override
+    public String getDescription() {
+      return null;
+    }
+  }
+}
diff --git a/solr/server/solr/solr.xml b/solr/server/solr/solr.xml
index d309f18cb40..bf364331214 100644
--- a/solr/server/solr/solr.xml
+++ b/solr/server/solr/solr.xml
@@ -33,6 +33,7 @@
   <str name="modules">${solr.modules:}</str>
   <str name="allowPaths">${solr.allowPaths:}</str>
   <str name="allowUrls">${solr.allowUrls:}</str>
+  <str name="hideStackTrace">${solr.hideStackTrace:false}</str>
 
   <solrcloud>
 
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
index 66fa7f8dc76..09dc697ed49 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
@@ -39,6 +39,7 @@ The default `solr.xml` file is found in `$SOLR_TIP/server/solr/solr.xml` and loo
   <str name="modules">${solr.modules:}</str>
   <str name="allowPaths">${solr.allowPaths:}</str>
   <str name="allowUrls">${solr.allowUrls:}</str>
+  <str name="hideStackTrace">${solr.hideStackTrace:false}</str>
 
   <solrcloud>
 
@@ -254,6 +255,15 @@ In SolrCloud mode, the allow-list is automatically configured to include all liv
 The allow-list can also be configured with the `solr.allowUrls` system property in `solr.in.sh` / `solr.in.cmd`.
 If you need to disable this feature for backwards compatibility, you can set the system property `solr.disable.allowUrls=true`.
 
+`hideStackTrace`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+When this attribute is set to `true`, Solr will not return any stack traces in the HTTP response in case of errors.  By default (`false`), stack traces are hidden only for predictable Solr exceptions, but are returned in the response for unexpected exceptions (i.e.: HTTP 500)
+
 `shareSchema`::
 +
 [%autowidth,frame=none]