You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2025/12/01 21:28:07 UTC

(solr) branch main updated: SOLR-17998: Improve error response, and RemoteSolrException (#1657)

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

houston 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 70c586f96d1 SOLR-17998: Improve error response, and RemoteSolrException (#1657)
70c586f96d1 is described below

commit 70c586f96d14cc724696172f6adb9057d4e77a6e
Author: Houston Putman <ho...@apache.org>
AuthorDate: Mon Dec 1 13:28:01 2025 -0800

    SOLR-17998: Improve error response, and RemoteSolrException (#1657)
    
    This is true for the v1 and v2 API. Various SolrException classes have been refactored to be more streamlined.
    
    JSON lists are (once again) indented if they are larger than 1 element.
---
 .../unreleased/SOLR-17998-better-error-info.yml    |  12 ++
 .../apache/solr/client/api/model/ErrorInfo.java    |  35 ++++-
 .../src/java/org/apache/solr/api/AnnotatedApi.java |   7 +-
 solr/core/src/java/org/apache/solr/api/ApiBag.java |  21 +--
 .../api/collections/CollectionHandlingUtils.java   |   6 +-
 .../org/apache/solr/handler/SchemaHandler.java     |   6 +-
 .../org/apache/solr/handler/SolrConfigHandler.java |   6 +-
 .../apache/solr/response/JSONResponseWriter.java   |   6 +
 .../apache/solr/response/JacksonJsonWriter.java    |  20 ++-
 .../solr/response/RawShimTextResponseWriter.java   |   5 +
 .../org/apache/solr/response/SchemaXmlWriter.java  |   5 +
 .../solr/response/TabularResponseWriter.java       |   3 +
 .../java/org/apache/solr/response/XMLWriter.java   |   5 +
 .../org/apache/solr/servlet/ResponseUtils.java     | 130 ++++++++++++------
 .../org/apache/solr/TestDistributedSearch.java     |   5 +-
 .../api/NodeConfigClusterPluginsSourceTest.java    |   4 +-
 .../embedded/TestEmbeddedSolrServerSchemaAPI.java  |   5 +-
 .../solr/cloud/CreateCollectionCleanupTest.java    |   4 +-
 .../solr/cloud/DistribJoinFromCollectionTest.java  |   7 +-
 .../apache/solr/cloud/LeaderTragicEventTest.java   |   2 +-
 .../org/apache/solr/cloud/MigrateRouteKeyTest.java |   5 +-
 .../apache/solr/cloud/MultiThreadedOCPTest.java    |   3 +-
 .../org/apache/solr/cloud/OverseerStatusTest.java  |   6 +-
 .../test/org/apache/solr/cloud/SplitShardTest.java |   5 +-
 .../solr/cloud/TestAuthenticationFramework.java    |   7 +-
 .../org/apache/solr/cloud/TestConfigSetsAPI.java   |   3 +-
 .../solr/cloud/TestConfigSetsAPIZkFailure.java     |   5 +-
 .../CollectionsAPIAsyncDistributedZkTest.java      |   3 +-
 .../solr/cloud/api/collections/ShardSplitTest.java |  11 +-
 .../cloud/api/collections/TestCollectionAPI.java   |  20 +--
 .../TestRequestStatusCollectionAPI.java            |   5 +-
 .../solr/filestore/TestDistribFileStore.java       |   4 +-
 .../apache/solr/handler/TestContainerPlugin.java   |  20 +--
 .../apache/solr/handler/V2ApiIntegrationTest.java  |   6 +-
 .../solr/handler/admin/CoreAdminHandlerTest.java   |   5 +-
 .../solr/handler/admin/HealthCheckHandlerTest.java |   9 +-
 .../solr/handler/admin/TestCollectionAPIs.java     |   6 +-
 .../handler/component/ShardsAllowListTest.java     |   3 +-
 .../src/test/org/apache/solr/pkg/TestPackages.java |  11 +-
 .../solr/response/TestErrorResponseStackTrace.java | 139 +++++++++++++++++++
 .../apache/solr/response/TestRawTransformer.java   |  20 ++-
 .../apache/solr/schema/SchemaApiFailureTest.java   |   4 +-
 .../solr/security/BasicAuthIntegrationTest.java    |  17 +--
 .../solr/servlet/TestRequestRateLimiter.java       |   6 +-
 .../processor/TextToVectorUpdateProcessorTest.java |   4 +-
 .../client/solrj/io/stream/JSONTupleStream.java    |   7 +-
 .../solr/client/solrj/RemoteSolrException.java     | 148 +++++++++++++++++++++
 .../org/apache/solr/client/solrj/SolrClient.java   |  17 ---
 .../solrj/impl/BaseHttpClusterStateProvider.java   |   3 +-
 .../solrj/impl/ConcurrentUpdateBaseSolrClient.java |  24 +---
 .../solr/client/solrj/impl/HttpSolrClientBase.java |  71 +++-------
 .../solr/client/solrj/impl/LBAsyncSolrClient.java  |   6 +-
 .../solr/client/solrj/impl/LBSolrClient.java       |   6 +-
 .../solrj/impl/RemoteExecutionException.java       |  59 --------
 .../solr/common/SolrErrorWrappingException.java    |  56 ++++++++
 .../java/org/apache/solr/common/SolrException.java |  17 ++-
 .../apache/solr/common/util/JsonTextWriter.java    |   7 +-
 .../org/apache/solr/common/util/TextWriter.java    |   5 +
 .../apache/solr/client/solrj/SolrExampleTests.java |  46 +++----
 .../solr/client/solrj/TestSolrJErrorHandling.java  |   2 +-
 .../solrj/impl/CloudHttp2SolrClientTest.java       |   5 +-
 .../client/solrj/impl/HttpJdkSolrClientTest.java   |   8 +-
 .../client/solrj/impl/HttpSolrClientTestBase.java  |  22 +--
 .../HttpJettySolrClientCompatibilityTest.java      |   8 +-
 .../solrj/jetty/HttpJettySolrClientTest.java       |  13 +-
 .../solr/client/solrj/request/SchemaTest.java      |   3 +-
 .../solrj/apache/ConcurrentUpdateSolrClient.java   |  21 +--
 .../solr/client/solrj/apache/HttpSolrClient.java   |  61 ++-------
 ...bstractCollectionsAPIDistributedZkTestBase.java |   3 +-
 .../api/collections/AbstractInstallShardTest.java  |   7 +-
 .../solrj/apache/BasicHttpSolrClientTest.java      |  46 +++----
 .../client/solrj/apache/CloudSolrClientTest.java   |   5 +-
 72 files changed, 818 insertions(+), 479 deletions(-)

diff --git a/changelog/unreleased/SOLR-17998-better-error-info.yml b/changelog/unreleased/SOLR-17998-better-error-info.yml
new file mode 100644
index 00000000000..d88319a4290
--- /dev/null
+++ b/changelog/unreleased/SOLR-17998-better-error-info.yml
@@ -0,0 +1,12 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Return structured error information in responses, parse correctly in SolrJ to give useful SolrExceptions
+type: changed # added, changed, fixed, deprecated, removed, dependency_update, security, other
+authors:
+  - name: Houston Putman
+    nick: HoustonPutman
+    url: https://home.apache.org/phonebook.html?uid=houston
+links:
+  - name: SOLR-17998
+    url: https://issues.apache.org/jira/browse/SOLR-17998
+  - name: PR#1382
+    url: https://github.com/apache/solr/pull/1382
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ErrorInfo.java b/solr/api/src/java/org/apache/solr/client/api/model/ErrorInfo.java
index 1a6ca071642..0d2d5fa9922 100644
--- a/solr/api/src/java/org/apache/solr/client/api/model/ErrorInfo.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/ErrorInfo.java
@@ -35,17 +35,20 @@ public class ErrorInfo implements ReflectWritable {
   @JsonProperty("metadata")
   public ErrorMetadata metadata;
 
-  @JsonProperty("details")
-  public List<Map<String, Object>> details;
+  @JsonProperty("code")
+  public Integer code;
+
+  @JsonProperty("errorClass")
+  public String errorClass;
 
   @JsonProperty("msg")
   public String msg;
 
   @JsonProperty("trace")
-  public String trace;
+  public ErrorStackTrace trace;
 
-  @JsonProperty("code")
-  public Integer code;
+  @JsonProperty("details")
+  public List<Map<String, Object>> details;
 
   public static class ErrorMetadata implements ReflectWritable {
     @JsonProperty(ERROR_CLASS)
@@ -54,4 +57,26 @@ public class ErrorInfo implements ReflectWritable {
     @JsonProperty(ROOT_ERROR_CLASS)
     public String rootErrorClass;
   }
+
+  public static class ErrorStackTrace implements ReflectWritable {
+    @JsonProperty("stackTrace")
+    public List<String> stackTrace;
+
+    @JsonProperty("causedBy")
+    public ErrorCausedBy causedBy;
+  }
+
+  public static class ErrorCausedBy implements ReflectWritable {
+    @JsonProperty("errorClass")
+    public String errorClass;
+
+    @JsonProperty("msg")
+    public String msg;
+
+    @JsonProperty("trace")
+    public ErrorStackTrace trace;
+
+    @JsonProperty("details")
+    public List<Map<String, Object>> details;
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
index d4c6e454520..76df23de362 100644
--- a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
+++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
@@ -36,6 +36,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SpecProvider;
 import org.apache.solr.common.util.CommandOperation;
@@ -207,7 +208,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider, Closeab
         fallback.call(req, rsp);
         return;
       } else {
-        throw new ApiBag.ExceptionWithErrObject(
+        throw new SolrErrorWrappingException(
             SolrException.ErrorCode.BAD_REQUEST,
             "Error processing commands",
             CommandOperation.captureErrors(cmds));
@@ -222,7 +223,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider, Closeab
     List<Map<String, Object>> errs = CommandOperation.captureErrors(cmds);
     if (!errs.isEmpty()) {
       log.error("{}{}", ERR, Utils.toJSONString(errs));
-      throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, ERR, errs);
+      throw new SolrErrorWrappingException(SolrException.ErrorCode.BAD_REQUEST, ERR, errs);
     }
   }
 
@@ -355,7 +356,7 @@ public class AnnotatedApi extends Api implements PermissionNameProvider, Closeab
 
     private void checkForErrorInPayload(CommandOperation cmd) {
       if (cmd.hasError()) {
-        throw new ApiBag.ExceptionWithErrObject(
+        throw new SolrErrorWrappingException(
             SolrException.ErrorCode.BAD_REQUEST,
             "Error executing command",
             CommandOperation.captureErrors(Collections.singletonList(cmd)));
diff --git a/solr/core/src/java/org/apache/solr/api/ApiBag.java b/solr/core/src/java/org/apache/solr/api/ApiBag.java
index 70a1964263a..26688037c21 100644
--- a/solr/core/src/java/org/apache/solr/api/ApiBag.java
+++ b/solr/core/src/java/org/apache/solr/api/ApiBag.java
@@ -37,6 +37,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SpecProvider;
 import org.apache.solr.common.util.CommandOperation;
@@ -448,30 +449,12 @@ public class ApiBag {
     }
     List<Map<String, Object>> errs = CommandOperation.captureErrors(commandsCopy);
     if (!errs.isEmpty()) {
-      throw new ExceptionWithErrObject(
+      throw new SolrErrorWrappingException(
           SolrException.ErrorCode.BAD_REQUEST, "Error in command payload", errs);
     }
     return commandsCopy;
   }
 
-  public static class ExceptionWithErrObject extends SolrException {
-    private final List<Map<String, Object>> errs;
-
-    public ExceptionWithErrObject(ErrorCode code, String msg, List<Map<String, Object>> errs) {
-      super(code, msg);
-      this.errs = errs;
-    }
-
-    public List<Map<String, Object>> getErrs() {
-      return errs;
-    }
-
-    @Override
-    public String getMessage() {
-      return super.getMessage() + ", errors: " + getErrs() + ", ";
-    }
-  }
-
   public static class LazyLoadedApi extends Api {
 
     private final PluginBag.PluginHolder<SolrRequestHandler> holder;
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
index 85440f4b650..c44a52e2e7d 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/CollectionHandlingUtils.java
@@ -36,7 +36,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
@@ -441,8 +441,8 @@ public class CollectionHandlingUtils {
       String shard,
       Set<String> okayExceptions) {
     String rootThrowable = null;
-    if (e instanceof SolrClient.RemoteSolrException) {
-      rootThrowable = ((SolrClient.RemoteSolrException) e).getRootThrowable();
+    if (e instanceof RemoteSolrException remoteSolrException) {
+      rootThrowable = remoteSolrException.getRootThrowable();
     }
 
     if (e != null && (rootThrowable == null || !okayExceptions.contains(rootThrowable))) {
diff --git a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
index b4362858daa..967054208b1 100644
--- a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
@@ -34,8 +34,8 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.api.JerseyResource;
+import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.SolrParams;
@@ -92,7 +92,7 @@ public class SchemaHandler extends RequestHandlerBase
         List<CommandOperation> ops = req.getCommands(false);
         List<Map<String, Object>> errs = CommandOperation.captureErrors(ops);
         if (!errs.isEmpty()) {
-          throw new ApiBag.ExceptionWithErrObject(
+          throw new SolrErrorWrappingException(
               SolrException.ErrorCode.BAD_REQUEST, "error processing commands", errs);
         }
 
@@ -102,7 +102,7 @@ public class SchemaHandler extends RequestHandlerBase
                 .collect(Collectors.toList());
         errs = new SchemaManager(req).performOperations(operationList);
         if (!errs.isEmpty()) {
-          throw new ApiBag.ExceptionWithErrObject(
+          throw new SolrErrorWrappingException(
               SolrException.ErrorCode.BAD_REQUEST, "error processing commands", errs);
         }
       } catch (IOException e) {
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index bbaf39b1931..652649e3669 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -53,7 +53,6 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.client.solrj.io.stream.expr.Expressible;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
@@ -62,6 +61,7 @@ import org.apache.solr.client.solrj.response.SimpleSolrResponse;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
 import org.apache.solr.common.MapSerializable;
+import org.apache.solr.common.SolrErrorWrappingException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
@@ -495,7 +495,7 @@ public class SolrConfigHandler extends RequestHandlerBase
       @SuppressWarnings({"rawtypes"})
       List errs = CommandOperation.captureErrors(ops);
       if (!errs.isEmpty()) {
-        throw new ApiBag.ExceptionWithErrObject(
+        throw new SolrErrorWrappingException(
             SolrException.ErrorCode.BAD_REQUEST, "error processing params", errs);
       }
 
@@ -577,7 +577,7 @@ public class SolrConfigHandler extends RequestHandlerBase
       List errs = CommandOperation.captureErrors(ops);
       if (!errs.isEmpty()) {
         log.error("ERROR:{}", Utils.toJSONString(errs));
-        throw new ApiBag.ExceptionWithErrObject(
+        throw new SolrErrorWrappingException(
             SolrException.ErrorCode.BAD_REQUEST, "error processing commands", errs);
       }
 
diff --git a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
index c4439c20d7e..de59421827d 100644
--- a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
@@ -251,6 +251,12 @@ public class JSONResponseWriter implements TextQueryResponseWriter {
       super.writeArray(name, val, raw);
     }
 
+    @Override
+    public void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException {
+      ifNeededWriteTypeAndValueKey("array");
+      super.writeArray(name, val, size, raw);
+    }
+
     @Override
     public void writeNull(String name) throws IOException {
       ifNeededWriteTypeAndValueKey("null");
diff --git a/solr/core/src/java/org/apache/solr/response/JacksonJsonWriter.java b/solr/core/src/java/org/apache/solr/response/JacksonJsonWriter.java
index f254f625a52..f5d66865723 100644
--- a/solr/core/src/java/org/apache/solr/response/JacksonJsonWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JacksonJsonWriter.java
@@ -35,11 +35,16 @@ import org.apache.solr.request.SolrQueryRequest;
 /** A JSON ResponseWriter that uses jackson. */
 public class JacksonJsonWriter implements TextQueryResponseWriter {
 
+  protected static final DefaultPrettyPrinter.Indenter SMALL_ARRAY_IDENTIFIER =
+      DefaultPrettyPrinter.NopIndenter.instance;
+  protected static final DefaultPrettyPrinter.Indenter LARGE_ARRAY_IDENTIFIER =
+      new DefaultIndenter().withLinefeed("\n");
+
   protected final JsonFactory jsonfactory;
   protected static final DefaultPrettyPrinter pretty =
       new DefaultPrettyPrinter()
           .withoutSpacesInObjectEntries()
-          .withArrayIndenter(DefaultPrettyPrinter.NopIndenter.instance)
+          .withArrayIndenter(SMALL_ARRAY_IDENTIFIER)
           .withObjectIndenter(new DefaultIndenter().withLinefeed("\n"));
 
   public JacksonJsonWriter() {
@@ -95,12 +100,16 @@ public class JacksonJsonWriter implements TextQueryResponseWriter {
   public static class WriterImpl extends JSONWriter {
 
     protected JsonGenerator gen;
+    private final DefaultPrettyPrinter prettyPrinter;
 
     public WriterImpl(SolrQueryRequest req, SolrQueryResponse rsp, JsonGenerator generator) {
       super(null, req, rsp);
       gen = generator;
       if (doIndent) {
-        gen.setPrettyPrinter(pretty.createInstance());
+        prettyPrinter = pretty.createInstance();
+        gen.setPrettyPrinter(prettyPrinter);
+      } else {
+        prettyPrinter = null;
       }
     }
 
@@ -187,6 +196,13 @@ public class JacksonJsonWriter implements TextQueryResponseWriter {
 
     @Override
     public void writeArrayOpener(int size) throws IOException, IllegalArgumentException {
+      if (prettyPrinter != null) {
+        if (size == 1 || size == 0) {
+          prettyPrinter.indentArraysWith(SMALL_ARRAY_IDENTIFIER);
+        } else {
+          prettyPrinter.indentArraysWith(LARGE_ARRAY_IDENTIFIER);
+        }
+      }
       gen.writeStartArray();
     }
 
diff --git a/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java b/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java
index fff644020f4..81099abba01 100644
--- a/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/RawShimTextResponseWriter.java
@@ -48,6 +48,11 @@ class RawShimTextResponseWriter extends TextResponseWriter {
     backing.writeArray(name, val, true);
   }
 
+  @Override
+  public void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException {
+    backing.writeArray(name, val, size, true);
+  }
+
   // Other stuff; just no-op delegation
   @Override
   public void writeStartDocumentList(
diff --git a/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java b/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
index 7ea7509564b..e6f96174fc2 100644
--- a/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/SchemaXmlWriter.java
@@ -402,6 +402,11 @@ public class SchemaXmlWriter extends TextResponseWriter {
     }
   }
 
+  @Override
+  public void writeArray(String name, Iterator<?> iter, int size, boolean raw) throws IOException {
+    writeArray(name, iter, raw);
+  }
+
   //
   // Primitive types
   //
diff --git a/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java b/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
index c05f6cef833..133b53405f3 100644
--- a/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/TabularResponseWriter.java
@@ -138,6 +138,9 @@ public abstract class TabularResponseWriter extends TextResponseWriter {
   @Override
   public void writeArray(String name, Iterator<?> val, boolean raw) throws IOException {}
 
+  @Override
+  public void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException {}
+
   @Override
   public void writeDate(String name, Date val) throws IOException {
     writeDate(name, val.toInstant().toString());
diff --git a/solr/core/src/java/org/apache/solr/response/XMLWriter.java b/solr/core/src/java/org/apache/solr/response/XMLWriter.java
index e8748e2ae78..708bc83e5d8 100644
--- a/solr/core/src/java/org/apache/solr/response/XMLWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/XMLWriter.java
@@ -300,6 +300,11 @@ public class XMLWriter extends TextResponseWriter {
     }
   }
 
+  @Override
+  public void writeArray(String name, Iterator<?> iter, int size, boolean raw) throws IOException {
+    writeArray(name, iter, raw);
+  }
+
   @Override
   public void writeIterator(String name, IteratorWriter val, boolean raw) throws IOException {
     // As the size is not known. So, always both startTag and endTag is written
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 f28b1c2cfea..dd4011a4dc0 100644
--- a/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
+++ b/solr/core/src/java/org/apache/solr/servlet/ResponseUtils.java
@@ -16,12 +16,12 @@
  */
 package org.apache.solr.servlet;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import org.apache.solr.api.ApiBag;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 import org.apache.solr.client.api.model.ErrorInfo;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
 import org.slf4j.Logger;
 
 /** Response helper methods. */
@@ -72,33 +72,50 @@ public class ResponseUtils {
       code = solrExc.code();
       NamedList<String> errorMetadata = solrExc.getMetadata();
       if (errorMetadata == null) {
-        errorMetadata = new NamedList<>();
+        errorMetadata = new SimpleOrderedMap<>();
       }
       errorMetadata.add(ErrorInfo.ERROR_CLASS, ex.getClass().getName());
       errorMetadata.add(
           ErrorInfo.ROOT_ERROR_CLASS, SolrException.getRootCause(ex).getClass().getName());
       info.add("metadata", errorMetadata);
-      if (ex instanceof ApiBag.ExceptionWithErrObject exception) {
-        info.add("details", exception.getErrs());
-      }
     }
 
-    for (Throwable th = ex; th != null; th = th.getCause()) {
-      String msg = th.getMessage();
-      if (msg != null) {
-        info.add("msg", msg);
-        break;
+    // For any regular code, don't include the stack trace
+    boolean printStackTrace = (code == 500 || code < 100) && !hideStackTrace(hideTrace);
+    boolean stackTracePrinted = false;
+    NamedList<Object> lastTrace = null;
+    Throwable causedBy = ex;
+    NamedList<Object> errorInfo = info;
+    while (causedBy != null) {
+      if (lastTrace != null) {
+        lastTrace.add("causedBy", errorInfo);
+      }
+      errorInfo.add("errorClass", causedBy.getClass().getName());
+      if (causedBy instanceof SolrException solrException) {
+        errorInfo.add("msg", solrException.getResponseMessage());
+        if (solrException.getDetails() != null) {
+          errorInfo.add("details", solrException.getDetails());
+        }
+      } else {
+        errorInfo.add("msg", causedBy.getMessage());
+      }
+      if (printStackTrace) {
+        stackTracePrinted = true;
+        lastTrace = new SimpleOrderedMap<>();
+        errorInfo.add("trace", lastTrace);
+        lastTrace.add(
+            "stackTrace",
+            Arrays.stream(causedBy.getStackTrace())
+                .map(StackTraceElement::toString)
+                .collect(Collectors.toList()));
+        causedBy = causedBy.getCause();
+        errorInfo = new SimpleOrderedMap<>();
+      } else {
+        causedBy = null;
       }
     }
 
-    // For any regular code, don't include the stack trace
     if (code == 500 || code < 100) {
-      // 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);
 
       // non standard codes have undefined results with various servers
@@ -107,6 +124,16 @@ public class ResponseUtils {
         code = 500;
       }
     }
+    if (!stackTracePrinted && ex != null && ex.getMessage() == null) {
+      // Find the first non-null message if we aren't printing the stacktrace
+      for (Throwable th = ex; th != null; th = th.getCause()) {
+        String msg = th.getMessage();
+        if (msg != null) {
+          info.add("msg", msg);
+          break;
+        }
+      }
+    }
 
     info.add("code", code);
     return code;
@@ -145,35 +172,60 @@ public class ResponseUtils {
       errorInfo.metadata = new ErrorInfo.ErrorMetadata();
       errorInfo.metadata.errorClass = ex.getClass().getName();
       errorInfo.metadata.rootErrorClass = SolrException.getRootCause(ex).getClass().getName();
-      if (ex instanceof ApiBag.ExceptionWithErrObject exception) {
-        errorInfo.details = exception.getErrs();
-      }
+      errorInfo.details = solrExc.getDetails();
+      errorInfo.msg = solrExc.getResponseMessage();
     }
 
-    for (Throwable th = ex; th != null; th = th.getCause()) {
-      String msg = th.getMessage();
-      if (msg != null) {
-        errorInfo.msg = msg;
-        break;
-      }
+    for (Throwable th = ex; errorInfo.msg == null && th != null; th = th.getCause()) {
+      errorInfo.msg = th.getMessage();
     }
 
     // For any regular code, don't include the stack trace
-    if (code == 500 || code < 100) {
-      if (!hideStackTrace(hideTrace)) {
-        StringWriter sw = new StringWriter();
-        ex.printStackTrace(new PrintWriter(sw));
-        errorInfo.trace = sw.toString();
-      }
-      log.error("500 Exception", ex);
+    boolean printStackTrace = (code == 500 || code < 100) && !hideStackTrace(hideTrace);
+    errorInfo.errorClass = ex.getClass().getName();
+    if (errorInfo.msg == null) {
+      errorInfo.msg = ex.getMessage();
+    }
 
-      // non standard codes have undefined results with various servers
-      if (code < 100) {
-        log.warn("invalid return code: {}", code);
-        code = 500;
+    if (printStackTrace) {
+      errorInfo.trace = new ErrorInfo.ErrorStackTrace();
+      errorInfo.trace.stackTrace =
+          Arrays.stream(ex.getStackTrace())
+              .map(StackTraceElement::toString)
+              .collect(Collectors.toList());
+
+      ErrorInfo.ErrorStackTrace lastTrace = errorInfo.trace;
+      Throwable causedBy = ex.getCause();
+
+      while (causedBy != null) {
+        ErrorInfo.ErrorCausedBy errorCausedBy = new ErrorInfo.ErrorCausedBy();
+        lastTrace.causedBy = errorCausedBy;
+        errorCausedBy.errorClass = causedBy.getClass().getName();
+        if (ex instanceof SolrException solrExc) {
+          errorCausedBy.msg = solrExc.getResponseMessage();
+          errorInfo.details = solrExc.getDetails();
+        } else {
+          errorCausedBy.msg = causedBy.getMessage();
+        }
+        errorCausedBy.trace = new ErrorInfo.ErrorStackTrace();
+        errorCausedBy.trace.stackTrace =
+            Arrays.stream(causedBy.getStackTrace())
+                .map(StackTraceElement::toString)
+                .collect(Collectors.toList());
+        lastTrace = errorCausedBy.trace;
+        causedBy = causedBy.getCause();
       }
     }
 
+    // non standard codes have undefined results with various servers
+    if (code < 100) {
+      log.warn("invalid return code: {}", code);
+      code = 500;
+    }
+    if (code == 500) {
+      log.error("500 Exception", ex);
+    }
+
     errorInfo.code = code;
     return errorInfo;
   }
diff --git a/solr/core/src/test/org/apache/solr/TestDistributedSearch.java b/solr/core/src/test/org/apache/solr/TestDistributedSearch.java
index 4b76ed51b17..3c823f6601e 100644
--- a/solr/core/src/test/org/apache/solr/TestDistributedSearch.java
+++ b/solr/core/src/test/org/apache/solr/TestDistributedSearch.java
@@ -32,6 +32,7 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorCompletionService;
 import java.util.concurrent.Future;
 import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.SolrQuery;
@@ -1716,8 +1717,8 @@ public class TestDistributedSearch extends BaseDistributedSearchTestCase {
           tdate_b,
           "stats.calcdistinct",
           "true");
-    } catch (SolrClient.RemoteSolrException e) {
-      if (e.getMessage().startsWith("java.lang.NullPointerException")) {
+    } catch (RemoteSolrException e) {
+      if (e.getMessage().contains("java.lang.NullPointerException")) {
         fail("NullPointerException with stats request on empty index");
       } else {
         throw e;
diff --git a/solr/core/src/test/org/apache/solr/api/NodeConfigClusterPluginsSourceTest.java b/solr/core/src/test/org/apache/solr/api/NodeConfigClusterPluginsSourceTest.java
index ad640ebb495..a50979b3418 100644
--- a/solr/core/src/test/org/apache/solr/api/NodeConfigClusterPluginsSourceTest.java
+++ b/solr/core/src/test/org/apache/solr/api/NodeConfigClusterPluginsSourceTest.java
@@ -20,7 +20,7 @@ package org.apache.solr.api;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import org.apache.solr.client.solrj.impl.RemoteExecutionException;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.request.V2Request;
 import org.apache.solr.client.solrj.request.beans.PluginMeta;
 import org.apache.solr.client.solrj.response.V2Response;
@@ -125,7 +125,7 @@ public class NodeConfigClusterPluginsSourceTest extends SolrCloudTestCase {
     try {
       req.process(cluster.getSolrClient());
       fail("Expected a 404 response code because the Edit Apis are not registered");
-    } catch (RemoteExecutionException e) {
+    } catch (RemoteSolrException e) {
       assertEquals(
           "Expected a HTTP 404 response code because the /cluster/plugin API should not be registered",
           404,
diff --git a/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerSchemaAPI.java b/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerSchemaAPI.java
index e7e22a065a4..c0a4a3aaecc 100644
--- a/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerSchemaAPI.java
+++ b/solr/core/src/test/org/apache/solr/client/solrj/embedded/TestEmbeddedSolrServerSchemaAPI.java
@@ -22,7 +22,6 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.ApiBag;
 import org.apache.solr.client.solrj.request.schema.SchemaRequest;
 import org.apache.solr.client.solrj.response.schema.SchemaResponse;
 import org.apache.solr.common.SolrException;
@@ -107,8 +106,8 @@ public class TestEmbeddedSolrServerSchemaAPI extends SolrTestCaseJ4 {
 
   private static void assertFailedSchemaResponse(
       ThrowingRunnable runnable, String expectedErrorMessage) {
-    ApiBag.ExceptionWithErrObject e = expectThrows(ApiBag.ExceptionWithErrObject.class, runnable);
-    String msg = e.getErrs().get(0).get("errorMessages").toString();
+    SolrException e = expectThrows(SolrException.class, runnable);
+    String msg = e.getDetails().get(0).get("errorMessages").toString();
     assertTrue(msg.contains(expectedErrorMessage));
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/cloud/CreateCollectionCleanupTest.java b/solr/core/src/test/org/apache/solr/cloud/CreateCollectionCleanupTest.java
index 92b61ce1217..d5c342e3629 100644
--- a/solr/core/src/test/org/apache/solr/cloud/CreateCollectionCleanupTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/CreateCollectionCleanupTest.java
@@ -24,7 +24,7 @@ import static org.hamcrest.CoreMatchers.not;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Properties;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.response.RequestStatusState;
@@ -84,7 +84,7 @@ public class CreateCollectionCleanupTest extends SolrCloudTestCase {
     properties.put(CoreAdminParams.DATA_DIR, tmpDir.toString());
     create.setProperties(properties);
     expectThrows(
-        SolrClient.RemoteSolrException.class,
+        RemoteSolrException.class,
         () -> {
           create.process(cloudClient);
         });
diff --git a/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java b/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java
index 0d2d7bef066..8a14d00ace8 100644
--- a/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/DistribJoinFromCollectionTest.java
@@ -25,7 +25,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@@ -224,9 +224,8 @@ public class DistribJoinFromCollectionTest extends SolrCloudTestCase {
             + " to=join_s}match_s:c";
     final QueryRequest qr =
         new QueryRequest(params("collection", toColl, "q", joinQ, "fl", "id,get_s,score"));
-    SolrClient.RemoteSolrException ex =
-        assertThrows(
-            SolrClient.RemoteSolrException.class, () -> cluster.getSolrClient().request(qr));
+    RemoteSolrException ex =
+        assertThrows(RemoteSolrException.class, () -> cluster.getSolrClient().request(qr));
     assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
     assertTrue(ex.getMessage().contains(wrongName));
   }
diff --git a/solr/core/src/test/org/apache/solr/cloud/LeaderTragicEventTest.java b/solr/core/src/test/org/apache/solr/cloud/LeaderTragicEventTest.java
index 40653362c5d..c1520025fe8 100644
--- a/solr/core/src/test/org/apache/solr/cloud/LeaderTragicEventTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/LeaderTragicEventTest.java
@@ -23,8 +23,8 @@ import static org.hamcrest.CoreMatchers.is;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import org.apache.lucene.store.AlreadyClosedException;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrClient.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
diff --git a/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java b/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
index 4c64bb8d624..c990f43423a 100644
--- a/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/MigrateRouteKeyTest.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
@@ -87,9 +88,9 @@ public class MigrateRouteKeyTest extends SolrCloudTestCase {
     CollectionAdminRequest.createCollection(targetCollection, "conf", 1, 1)
         .process(cluster.getSolrClient());
 
-    SolrClient.RemoteSolrException remoteSolrException =
+    RemoteSolrException remoteSolrException =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             "Expected an exception in case split.key is not specified",
             () -> {
               CollectionAdminRequest.migrateData(sourceCollection, targetCollection, "")
diff --git a/solr/core/src/test/org/apache/solr/cloud/MultiThreadedOCPTest.java b/solr/core/src/test/org/apache/solr/cloud/MultiThreadedOCPTest.java
index 833f28a1a63..6a8e56cbb51 100644
--- a/solr/core/src/test/org/apache/solr/cloud/MultiThreadedOCPTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/MultiThreadedOCPTest.java
@@ -21,6 +21,7 @@ import static org.apache.solr.cloud.OverseerTaskProcessor.MAX_PARALLEL_TASKS;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.Random;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@@ -259,7 +260,7 @@ public class MultiThreadedOCPTest extends AbstractFullDistribZkTestBase {
       // Now submit another task with the same id. At this time, hopefully the previous 3002 should
       // still be in the queue.
       expectThrows(
-          SolrClient.RemoteSolrException.class,
+          RemoteSolrException.class,
           () -> {
             CollectionAdminRequest.splitShard("ocptest_shardsplit2")
                 .setShardName(SHARD1)
diff --git a/solr/core/src/test/org/apache/solr/cloud/OverseerStatusTest.java b/solr/core/src/test/org/apache/solr/cloud/OverseerStatusTest.java
index 8e99d751854..f8883662e74 100644
--- a/solr/core/src/test/org/apache/solr/cloud/OverseerStatusTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/OverseerStatusTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.solr.cloud;
 
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.util.NamedList;
@@ -89,9 +89,9 @@ public class OverseerStatusTest extends SolrCloudTestCase {
             collection_operations.get(CollectionParams.CollectionAction.RELOAD.toLower());
     assertEquals("No stats for reload in OverseerCollectionProcessor", 1, reload.get("requests"));
 
-    SolrClient.RemoteSolrException e =
+    RemoteSolrException e =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             "Split shard for non existent collection should have failed",
             () ->
                 CollectionAdminRequest.splitShard("non_existent_collection")
diff --git a/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java b/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
index 1fc7baa77d8..9018a32e294 100644
--- a/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/SplitShardTest.java
@@ -31,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
@@ -101,7 +102,7 @@ public class SplitShardTest extends SolrCloudTestCase {
               .setNumSubShards(10);
       splitShard.process(cluster.getSolrClient());
       fail("SplitShard should throw an exception when numSubShards > 8");
-    } catch (SolrClient.RemoteSolrException ex) {
+    } catch (RemoteSolrException ex) {
       assertTrue(
           ex.getMessage()
               .contains("A shard can only be split into 2 to 8 subshards in one split request."));
@@ -114,7 +115,7 @@ public class SplitShardTest extends SolrCloudTestCase {
               .setNumSubShards(1);
       splitShard.process(cluster.getSolrClient());
       fail("SplitShard should throw an exception when numSubShards < 2");
-    } catch (SolrClient.RemoteSolrException ex) {
+    } catch (RemoteSolrException ex) {
       assertTrue(
           ex.getMessage()
               .contains(
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java b/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java
index 88c80d73f33..70fb6eb9812 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestAuthenticationFramework.java
@@ -22,7 +22,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import java.lang.invoke.MethodHandles;
 import java.util.Map;
 import org.apache.http.HttpRequestInterceptor;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@@ -72,9 +72,8 @@ public class TestAuthenticationFramework extends SolrCloudTestCase {
 
     // Should fail with 401
     try {
-      SolrClient.RemoteSolrException e =
-          expectThrows(
-              SolrClient.RemoteSolrException.class, this::collectionCreateSearchDeleteTwice);
+      RemoteSolrException e =
+          expectThrows(RemoteSolrException.class, this::collectionCreateSearchDeleteTwice);
       assertTrue("Should've returned a 401 error", e.getMessage().contains("Error 401"));
     } finally {
       MockAuthenticationPlugin.expectedUsername = null;
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
index e318ee02821..2909539433f 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
@@ -66,6 +66,7 @@ import org.apache.http.message.BasicHeader;
 import org.apache.http.util.EntityUtils;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
@@ -1078,7 +1079,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
     ignoreException("uploaded without any authentication in place");
     Throwable thrown =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               createCollection(
                   "newcollection2",
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIZkFailure.java b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIZkFailure.java
index f271850fd62..0b8ccf57b7a 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIZkFailure.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIZkFailure.java
@@ -35,6 +35,7 @@ import org.apache.jute.InputArchive;
 import org.apache.jute.OutputArchive;
 import org.apache.jute.Record;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.request.ConfigSetAdminRequest.Create;
 import org.apache.solr.common.SolrException;
@@ -127,8 +128,8 @@ public class TestConfigSetsAPIZkFailure extends SolrTestCaseJ4 {
 
       Create create = new Create();
       create.setBaseConfigSetName(BASE_CONFIGSET_NAME).setConfigSetName(CONFIGSET_NAME);
-      SolrClient.RemoteSolrException se =
-          expectThrows(SolrClient.RemoteSolrException.class, () -> create.process(solrClient));
+      RemoteSolrException se =
+          expectThrows(RemoteSolrException.class, () -> create.process(solrClient));
       // partial creation should have been cleaned up
       assertFalse(configSetService.checkConfigExists(CONFIGSET_NAME));
       assertEquals(SolrException.ErrorCode.SERVER_ERROR.code, se.code());
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionsAPIAsyncDistributedZkTest.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionsAPIAsyncDistributedZkTest.java
index bdc47a6a82e..6eae89d164f 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionsAPIAsyncDistributedZkTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/CollectionsAPIAsyncDistributedZkTest.java
@@ -27,6 +27,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.lucene.tests.util.TestUtil;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
@@ -273,7 +274,7 @@ public class CollectionsAPIAsyncDistributedZkTest extends SolrCloudTestCase {
                   reloadCollectionRequest.processAsync(
                       "repeatedId", clients[random().nextInt(clients.length)]);
                   numSuccess.incrementAndGet();
-                } catch (SolrServerException | SolrClient.RemoteSolrException e) {
+                } catch (SolrServerException | RemoteSolrException e) {
                   if (log.isInfoEnabled()) {
                     log.info("Exception during collection reloading, we were waiting for one: ", e);
                   }
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
index eb7c37fec5c..5092a8aca24 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/ShardSplitTest.java
@@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
@@ -801,7 +802,7 @@ public class ShardSplitTest extends BasicDistributedZkTest {
     try {
       splitShard(AbstractFullDistribZkTestBase.DEFAULT_COLLECTION, SHARD1, subRanges, null, false);
       fail("Shard splitting with just one custom hash range should not succeed");
-    } catch (SolrClient.RemoteSolrException e) {
+    } catch (RemoteSolrException e) {
       log.info("Expected exception:", e);
     }
     subRanges.clear();
@@ -812,7 +813,7 @@ public class ShardSplitTest extends BasicDistributedZkTest {
     try {
       splitShard(AbstractFullDistribZkTestBase.DEFAULT_COLLECTION, SHARD1, subRanges, null, false);
       fail("Shard splitting with missing hashes in between given ranges should not succeed");
-    } catch (SolrClient.RemoteSolrException e) {
+    } catch (RemoteSolrException e) {
       log.info("Expected exception:", e);
     }
     subRanges.clear();
@@ -825,7 +826,7 @@ public class ShardSplitTest extends BasicDistributedZkTest {
     try {
       splitShard(AbstractFullDistribZkTestBase.DEFAULT_COLLECTION, SHARD1, subRanges, null, false);
       fail("Shard splitting with overlapping ranges should not succeed");
-    } catch (SolrClient.RemoteSolrException e) {
+    } catch (RemoteSolrException e) {
       log.info("Expected exception:", e);
     }
     subRanges.clear();
@@ -920,7 +921,7 @@ public class ShardSplitTest extends BasicDistributedZkTest {
           log.info("Layout after split: \n");
           printLayout();
           break;
-        } catch (SolrClient.RemoteSolrException e) {
+        } catch (RemoteSolrException e) {
           if (e.code() != 500) {
             throw e;
           }
@@ -1139,7 +1140,7 @@ public class ShardSplitTest extends BasicDistributedZkTest {
       try {
         splitShard(collectionName, shardId, null, splitKey, false);
         break;
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         if (e.code() != 500) {
           throw e;
         }
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
index 0623a459acf..9d8f1c60469 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
@@ -26,7 +26,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.LongAdder;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -171,9 +171,9 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail("Trying to unset an unknown property should have failed");
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         // expected
-        assertTrue(e.getMessage().contains("no supported values provided"));
+        assertTrue(e.toString().contains("no supported values provided"));
       }
     }
   }
@@ -193,7 +193,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail();
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         final String errorMessage = e.getMessage();
         assertTrue(
             errorMessage.contains(
@@ -1149,7 +1149,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail();
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         final String errorMessage = e.getMessage();
         assertTrue(errorMessage.contains("Invalid collection"));
         assertTrue(errorMessage.contains("invalid@name#with$weird%characters"));
@@ -1172,7 +1172,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail();
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         final String errorMessage = e.getMessage();
         assertTrue(errorMessage.contains("Invalid shard"));
         assertTrue(errorMessage.contains("invalid@name#with$weird%characters"));
@@ -1193,7 +1193,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail();
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         final String errorMessage = e.getMessage();
         assertTrue(errorMessage.contains("Invalid alias"));
         assertTrue(errorMessage.contains("invalid@name#with$weird%characters"));
@@ -1226,7 +1226,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
       try {
         client.request(request);
         fail();
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         final String errorMessage = e.getMessage();
         assertTrue(errorMessage.contains("Invalid shard"));
         assertTrue(errorMessage.contains("invalid@name#with$weird%characters"));
@@ -1298,9 +1298,9 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
 
     try (CloudSolrClient client = createCloudClient(null)) {
       // first, try creating a collection with badconf
-      SolrClient.RemoteSolrException rse =
+      RemoteSolrException rse =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 CollectionAdminRequest.createCollection("testcollection", "badconf", 1, 2)
                     .process(client);
diff --git a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestRequestStatusCollectionAPI.java b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestRequestStatusCollectionAPI.java
index 1e6e0681808..b48ae59b668 100644
--- a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestRequestStatusCollectionAPI.java
+++ b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestRequestStatusCollectionAPI.java
@@ -24,6 +24,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
@@ -171,9 +172,9 @@ public class TestRequestStatusCollectionAPI extends BasicDistributedZkTest {
     duplicateRequestIdParams.set("collection.configName", "conf1");
     duplicateRequestIdParams.set(CommonAdminParams.ASYNC, "1002");
 
-    final SolrClient.RemoteSolrException thrown =
+    final RemoteSolrException thrown =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               sendRequest(duplicateRequestIdParams);
             });
diff --git a/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java b/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
index 0efdb3ca25d..a4de2935534 100644
--- a/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
+++ b/solr/core/src/test/org/apache/solr/filestore/TestDistribFileStore.java
@@ -34,12 +34,12 @@ import java.util.concurrent.Callable;
 import java.util.function.Predicate;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.http.client.methods.HttpDelete;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
-import org.apache.solr.client.solrj.impl.RemoteExecutionException;
 import org.apache.solr.client.solrj.request.FileStoreApi;
 import org.apache.solr.client.solrj.request.V2Request;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
@@ -94,7 +94,7 @@ public class TestDistribFileStore extends SolrCloudTestCase {
             "/package/mypkg/v1.0/runtimelibs.jar",
             "j+Rflxi64tXdqosIhbusqi6GTwZq8znunC/dzwcWW0/dHlFGKDurOaE1Nz9FSPJuXbHkVLj638yZ0Lp1ssnoYA==");
         fail("should have failed because of wrong signature ");
-      } catch (RemoteExecutionException e) {
+      } catch (RemoteSolrException e) {
         assertThat(e.getMessage(), containsString("Signature does not match"));
       }
 
diff --git a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
index 7f913a513f8..f7f0560b6f3 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestContainerPlugin.java
@@ -39,8 +39,8 @@ import org.apache.solr.api.Command;
 import org.apache.solr.api.ConfigurablePlugin;
 import org.apache.solr.api.ContainerPluginsRegistry;
 import org.apache.solr.api.EndPoint;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.impl.RemoteExecutionException;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.V2Request;
 import org.apache.solr.client.solrj.request.beans.PackagePayload;
@@ -51,6 +51,7 @@ import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.annotation.JsonProperty;
 import org.apache.solr.common.util.ReflectMapWriter;
+import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.SolrResourceLoader;
 import org.apache.solr.embedded.JettySolrRunner;
@@ -212,12 +213,11 @@ public class TestContainerPlugin extends SolrCloudTestCase {
 
     version = phaser.awaitAdvanceInterruptibly(version, 10, TimeUnit.SECONDS);
 
-    RemoteExecutionException e =
+    RemoteSolrException e =
         assertThrows(
-            RemoteExecutionException.class,
-            () -> getPlugin("/my-random-prefix/their/plugin").call());
+            RemoteSolrException.class, () -> getPlugin("/my-random-prefix/their/plugin").call());
     assertEquals(404, e.code());
-    final String msg = (String) ((Map<String, Object>) (e.getMetaData().get("error"))).get("msg");
+    final String msg = Utils.getObjectByPath(e.getRemoteErrorObject(), false, "msg").toString();
     assertThat(msg, containsString("Cannot find API for the path"));
 
     // test ClusterSingleton plugin
@@ -580,15 +580,17 @@ public class TestContainerPlugin extends SolrCloudTestCase {
   }
 
   private void expectError(V2Request req, String expectErrorMsg) {
-    String errPath = "/error/details[0]/errorMessages[0]";
+    String errPath = "details[0]/errorMessages[0]";
     expectError(req, cluster.getSolrClient(), errPath, expectErrorMsg);
   }
 
   private static void expectError(
       V2Request req, SolrClient client, String errPath, String expectErrorMsg) {
-    RemoteExecutionException e =
-        expectThrows(RemoteExecutionException.class, () -> req.process(client));
-    String msg = Objects.requireNonNullElse(e.getMetaData()._getStr(errPath), "");
+    RemoteSolrException e = expectThrows(RemoteSolrException.class, () -> req.process(client));
+    String msg =
+        Objects.requireNonNullElse(
+                Utils.getObjectByPath(e.getRemoteErrorObject(), false, errPath), "")
+            .toString();
     assertTrue(expectErrorMsg, msg.contains(expectErrorMsg));
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index e00a0bd2e9b..3b7a85eb096 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -26,7 +26,7 @@ import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.client.utils.URIBuilder;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.CloudLegacySolrClient;
@@ -79,9 +79,9 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase {
             .withPayload(payload)
             .build();
     v2Request.setResponseParser(responseParser);
-    SolrClient.RemoteSolrException ex =
+    RemoteSolrException ex =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               v2Request.process(cluster.getSolrClient());
             });
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerTest.java
index 6feae5d21f5..33f3d9c4da5 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerTest.java
@@ -33,6 +33,7 @@ import org.apache.commons.io.file.PathUtils;
 import org.apache.lucene.util.Constants;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.model.CoreStatusResponse;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
 import org.apache.solr.client.solrj.request.CoreAdminRequest;
@@ -421,9 +422,9 @@ public class CoreAdminHandlerTest extends SolrTestCaseJ4 {
       req.process(client);
     }
 
-    SolrClient.RemoteSolrException rse =
+    RemoteSolrException rse =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               try (SolrClient client =
                   new HttpSolrClient.Builder(runner.getBaseUrl().toString())
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/HealthCheckHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/HealthCheckHandlerTest.java
index ced47e30c78..43838707d05 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/HealthCheckHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/HealthCheckHandlerTest.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Properties;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -102,9 +103,9 @@ public class HealthCheckHandlerTest extends SolrCloudTestCase {
       newJetty.getCoreContainer().getZkController().getZkClient().close();
 
       // negative check of our (new) "broken" node that we deliberately put into an unhealthy state
-      SolrClient.RemoteSolrException e =
+      RemoteSolrException e =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 runHealthcheckWithClient(solrClient);
               });
@@ -157,9 +158,9 @@ public class HealthCheckHandlerTest extends SolrCloudTestCase {
       newJetty.getCoreContainer().getZkController().getZkClient().close();
 
       // negative check of our (new) "broken" node that we deliberately put into an unhealthy state
-      SolrClient.RemoteSolrException e =
+      RemoteSolrException e =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 new V2Request.Builder("/node/health").build().process(solrClient);
               });
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
index a8dde0459f5..7e6529ac269 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
@@ -169,11 +169,7 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
             return method.toString();
           }
         };
-    try {
-      api.call(req, rsp);
-    } catch (ApiBag.ExceptionWithErrObject e) {
-      throw new RuntimeException(e.getMessage() + Utils.toJSONString(e.getErrs()), e);
-    }
+    api.call(req, rsp);
     return new Pair<>(req, rsp);
   }
 
diff --git a/solr/core/src/test/org/apache/solr/handler/component/ShardsAllowListTest.java b/solr/core/src/test/org/apache/solr/handler/component/ShardsAllowListTest.java
index d6b19fad4f1..57ff3e91958 100644
--- a/solr/core/src/test/org/apache/solr/handler/component/ShardsAllowListTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/component/ShardsAllowListTest.java
@@ -27,6 +27,7 @@ import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.SolrQuery;
 import org.apache.solr.cloud.MiniSolrCloudCluster;
@@ -270,7 +271,7 @@ public class ShardsAllowListTest extends MultiSolrCloudTestCase {
       numDocs(query, shards, cluster);
       fail("Expecting failure for shards parameter: '" + shards + "'");
     } catch (SolrServerException e) {
-      assertThat(e.getCause(), instanceOf(SolrException.class));
+      assertThat(e.getCause(), instanceOf(RemoteSolrException.class));
       assertThat(((SolrException) e.getCause()).code(), is(SolrException.ErrorCode.FORBIDDEN.code));
       assertThat(e.getCause().getMessage(), containsString(expectedExceptionMessage));
     } finally {
diff --git a/solr/core/src/test/org/apache/solr/pkg/TestPackages.java b/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
index 9a7c084f6b6..88113d83cf8 100644
--- a/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
+++ b/solr/core/src/test/org/apache/solr/pkg/TestPackages.java
@@ -42,12 +42,12 @@ import org.apache.lucene.analysis.core.WhitespaceTokenizerFactory;
 import org.apache.lucene.analysis.pattern.PatternReplaceCharFilterFactory;
 import org.apache.lucene.util.ResourceLoader;
 import org.apache.lucene.util.ResourceLoaderAware;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.apache.HttpClientUtil;
 import org.apache.solr.client.solrj.apache.HttpSolrClient;
-import org.apache.solr.client.solrj.impl.RemoteExecutionException;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.RequestWriter;
@@ -579,7 +579,7 @@ public class TestPackages extends SolrCloudTestCase {
   @Test
   @SuppressWarnings("unchecked")
   public void testAPI() throws Exception {
-    String errPath = "/error/details[0]/errorMessages[0]";
+    String errPath = "/details[0]/errorMessages[0]";
     String FILE1 = "/mypkg/v.0.12/jar_a.jar";
     String FILE2 = "/mypkg/v.0.12/jar_b.jar";
     String FILE3 = "/mypkg/v.0.13/jar_a.jar";
@@ -877,8 +877,11 @@ public class TestPackages extends SolrCloudTestCase {
     try {
       req.process(client);
       fail("should have failed with message : " + expectErrorMsg);
-    } catch (RemoteExecutionException e) {
-      String msg = Objects.requireNonNullElse(e.getMetaData()._getStr(errPath), "");
+    } catch (RemoteSolrException e) {
+      String msg =
+          Objects.requireNonNullElse(
+                  Utils.getObjectByPath(e.getRemoteErrorObject(), false, errPath), "")
+              .toString();
       assertTrue(
           "should have failed with message: " + expectErrorMsg + "actual message : " + msg,
           msg.contains(expectErrorMsg));
diff --git a/solr/core/src/test/org/apache/solr/response/TestErrorResponseStackTrace.java b/solr/core/src/test/org/apache/solr/response/TestErrorResponseStackTrace.java
new file mode 100644
index 00000000000..ce10d4e2d48
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/response/TestErrorResponseStackTrace.java
@@ -0,0 +1,139 @@
+/*
+ * 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.response;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.commons.codec.CharEncoding;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.lucene.tests.util.LuceneTestCase;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.RemoteSolrException;
+import org.apache.solr.client.solrj.apache.HttpClientUtil;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.response.json.JsonMapResponseParser;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.handler.component.ResponseBuilder;
+import org.apache.solr.handler.component.SearchComponent;
+import org.apache.solr.util.SolrJettyTestRule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+@SuppressSSL
+public class TestErrorResponseStackTrace extends SolrTestCaseJ4 {
+
+  @ClassRule public static final SolrJettyTestRule solrRule = new SolrJettyTestRule();
+
+  @BeforeClass
+  public static void setupSolrHome() throws Exception {
+    Path configSet = createTempDir("configSet");
+    copyMinConf(configSet);
+    // 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.response.TestErrorResponseStackTrace$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();
+  }
+
+  @Test
+  public void testFullStackTrace() throws Exception {
+    final String url = solrRule.getBaseUrl().toString() + "/collection1/withError?q=*:*&wt=json";
+    final HttpGet get = new HttpGet(url);
+    var client = HttpClientUtil.createClient(null);
+    try (CloseableHttpResponse responseRaw = client.execute(get)) {
+
+      assertEquals(500, responseRaw.getStatusLine().getStatusCode());
+      NamedList<Object> response =
+          new JsonMapResponseParser()
+              .processResponse(responseRaw.getEntity().getContent(), CharEncoding.UTF_8);
+
+      assertEquals(500L, response._get("error/code"));
+      assertEquals("java.lang.RuntimeException", response._get("error/errorClass"));
+      assertEquals("Stacktrace should be populated.", response._get("error/msg"));
+      assertTrue(
+          ((String) response._get("error/trace/stackTrace[0]"))
+              .contains("org.apache.solr.response.TestErrorResponseStackTrace$ErrorComponent"));
+      assertEquals("java.io.IOException", response._get("error/trace/causedBy/errorClass"));
+      assertEquals("This is the cause", response._get("error/trace/causedBy/msg"));
+      assertTrue(
+          ((String) response._get("error/trace/causedBy/trace/stackTrace[0]"))
+              .contains("org.apache.solr.response.TestErrorResponseStackTrace$ErrorComponent"));
+    } finally {
+      HttpClientUtil.close(client);
+    }
+  }
+
+  @Test
+  @SuppressWarnings({"unchecked"})
+  public void testRemoteSolrException() {
+    var client = solrRule.getSolrClient("collection1");
+    QueryRequest queryRequest =
+        new QueryRequest(new ModifiableSolrParams().set("q", "*:*").set("wt", "json"));
+    queryRequest.setPath("/withError");
+    RemoteSolrException exception =
+        expectThrows(RemoteSolrException.class, () -> queryRequest.process(client, "collection1"));
+    assertTrue(exception.getRemoteErrorObject() instanceof NamedList);
+    var remoteError = (NamedList<Object>) exception.getRemoteErrorObject();
+    assertEquals(500, remoteError._get("code"));
+    assertEquals("java.lang.RuntimeException", remoteError._get("errorClass"));
+    assertEquals("Stacktrace should be populated.", remoteError._get("msg"));
+    assertTrue(
+        ((String) remoteError._get("trace/stackTrace[0]"))
+            .contains("org.apache.solr.response.TestErrorResponseStackTrace$ErrorComponent"));
+    assertEquals("java.io.IOException", remoteError._get("trace/causedBy/errorClass"));
+    assertEquals("This is the cause", remoteError._get("trace/causedBy/msg"));
+    assertTrue(
+        ((String) remoteError._get("trace/causedBy/trace/stackTrace[0]"))
+            .contains("org.apache.solr.response.TestErrorResponseStackTrace$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(
+          "Stacktrace should be populated.", new IOException("This is the cause"));
+    }
+
+    @Override
+    public String getDescription() {
+      return null;
+    }
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java b/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
index 469558a4599..f806032b7b4 100644
--- a/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
+++ b/solr/core/src/test/org/apache/solr/response/TestRawTransformer.java
@@ -184,7 +184,9 @@ public class TestRawTransformer extends SolrCloudTestCase {
                     "fl",
                     new String[] {"author:[xml],content_type:[xml]"},
                     "wt",
-                    new String[] {"xml"})));
+                    new String[] {"xml"},
+                    "indent",
+                    new String[] {"false"})));
     req.setResponseParser(XML_STREAM_RESPONSE_PARSER);
     NamedList<Object> rsp = CLIENT.request(req, "collection1");
     String strResponse = InputStreamResponseParser.consumeResponseToString(rsp);
@@ -209,7 +211,9 @@ public class TestRawTransformer extends SolrCloudTestCase {
                     "fl",
                     new String[] {"author,content_type"},
                     "wt",
-                    new String[] {"xml"})));
+                    new String[] {"xml"},
+                    "indent",
+                    new String[] {"false"})));
     req.setResponseParser(XML_STREAM_RESPONSE_PARSER);
     rsp = CLIENT.request(req, "collection1");
     strResponse = InputStreamResponseParser.consumeResponseToString(rsp);
@@ -232,7 +236,9 @@ public class TestRawTransformer extends SolrCloudTestCase {
                     "fl",
                     new String[] {"author:[xml],content_type:[xml]"},
                     "wt",
-                    new String[] {"json"})));
+                    new String[] {"json"},
+                    "indent",
+                    new String[] {"false"})));
     req.setResponseParser(JSON_STREAM_RESPONSE_PARSER);
     rsp = CLIENT.request(req, "collection1");
     strResponse = InputStreamResponseParser.consumeResponseToString(rsp);
@@ -257,7 +263,9 @@ public class TestRawTransformer extends SolrCloudTestCase {
                     "fl",
                     new String[] {"subject:[json],links:[json]"},
                     "wt",
-                    new String[] {"json"})));
+                    new String[] {"json"},
+                    "indent",
+                    new String[] {"false"})));
     req.setResponseParser(JSON_STREAM_RESPONSE_PARSER);
     NamedList<Object> rsp = CLIENT.request(req, "collection1");
     String strResponse = InputStreamResponseParser.consumeResponseToString(rsp);
@@ -280,7 +288,9 @@ public class TestRawTransformer extends SolrCloudTestCase {
                     "fl",
                     new String[] {"id", "subject,links"},
                     "wt",
-                    new String[] {"json"})));
+                    new String[] {"json"},
+                    "indent",
+                    new String[] {"false"})));
     req.setResponseParser(JSON_STREAM_RESPONSE_PARSER);
     rsp = CLIENT.request(req, "collection1");
     strResponse = InputStreamResponseParser.consumeResponseToString(rsp);
diff --git a/solr/core/src/test/org/apache/solr/schema/SchemaApiFailureTest.java b/solr/core/src/test/org/apache/solr/schema/SchemaApiFailureTest.java
index f9267d27db2..3837d21bc7a 100644
--- a/solr/core/src/test/org/apache/solr/schema/SchemaApiFailureTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/SchemaApiFailureTest.java
@@ -21,7 +21,7 @@ import static org.hamcrest.Matchers.containsStringIgnoringCase;
 
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
 import org.apache.solr.client.solrj.request.schema.SchemaRequest;
@@ -57,7 +57,7 @@ public class SchemaApiFailureTest extends SolrCloudTestCase {
 
     final var thrown =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               fieldAddition.process(client, COLLECTION);
             });
diff --git a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
index 1bd45efc256..453d96401e6 100644
--- a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java
@@ -34,6 +34,7 @@ import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ByteArrayEntity;
 import org.apache.solr.cli.CLITestHelper;
 import org.apache.solr.cli.StatusTool;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -153,9 +154,9 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
       httpClient.getConnectionManager().closeExpiredConnections();
       httpClient.getConnectionManager().closeIdleConnections(1, TimeUnit.MILLISECONDS);
 
-      SolrClient.RemoteSolrException exp =
+      RemoteSolrException exp =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 cluster.getSolrClient().request(genericReq);
               });
@@ -222,9 +223,9 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
       CollectionAdminRequest.Reload reload = CollectionAdminRequest.reloadCollection(COLLECTION);
 
       try (SolrClient solrClient = getHttpSolrClient(baseUrl)) {
-        expectThrows(SolrClient.RemoteSolrException.class, () -> solrClient.request(reload));
+        expectThrows(RemoteSolrException.class, () -> solrClient.request(reload));
         reload.setMethod(SolrRequest.METHOD.POST);
-        expectThrows(SolrClient.RemoteSolrException.class, () -> solrClient.request(reload));
+        expectThrows(RemoteSolrException.class, () -> solrClient.request(reload));
       }
       cluster
           .getSolrClient()
@@ -233,7 +234,7 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
                   .setBasicAuthCredentials("harry", "HarryIsUberCool"));
 
       expectThrows(
-          SolrClient.RemoteSolrException.class,
+          RemoteSolrException.class,
           () -> {
             cluster
                 .getSolrClient()
@@ -264,9 +265,9 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
       delQuery.setBasicAuthCredentials("harry", "HarryIsUberCool");
       delQuery.process(aNewClient, COLLECTION); // this should succeed
       try {
-        SolrClient.RemoteSolrException e =
+        RemoteSolrException e =
             expectThrows(
-                SolrClient.RemoteSolrException.class,
+                RemoteSolrException.class,
                 () -> {
                   new UpdateRequest().deleteByQuery("*:*").process(aNewClient, COLLECTION);
                 });
@@ -305,7 +306,7 @@ public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
       // Query that fails due to missing credentials
       exp =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 cluster.getSolrClient().query(COLLECTION, params);
               });
diff --git a/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java b/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
index c9caca31b2f..fd29c164b02 100644
--- a/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
+++ b/solr/core/src/test/org/apache/solr/servlet/TestRequestRateLimiter.java
@@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.LongAdder;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
@@ -376,9 +377,8 @@ public class TestRequestRateLimiter extends SolrCloudTestCase {
         try {
           assertNotNull(future.get());
         } catch (ExecutionException e) {
-          assertThat(e.getCause().getCause(), instanceOf(SolrClient.RemoteSolrException.class));
-          SolrClient.RemoteSolrException rse =
-              (SolrClient.RemoteSolrException) e.getCause().getCause();
+          assertThat(e.getCause().getCause(), instanceOf(RemoteSolrException.class));
+          RemoteSolrException rse = (RemoteSolrException) e.getCause().getCause();
           assertEquals(SolrException.ErrorCode.TOO_MANY_REQUESTS.code, rse.code());
           assertThat(
               rse.getMessage(), containsString("non ok status: 429, message:Too Many Requests"));
diff --git a/solr/modules/llm/src/test/org/apache/solr/llm/textvectorisation/update/processor/TextToVectorUpdateProcessorTest.java b/solr/modules/llm/src/test/org/apache/solr/llm/textvectorisation/update/processor/TextToVectorUpdateProcessorTest.java
index 602659b276c..c82d2312977 100644
--- a/solr/modules/llm/src/test/org/apache/solr/llm/textvectorisation/update/processor/TextToVectorUpdateProcessorTest.java
+++ b/solr/modules/llm/src/test/org/apache/solr/llm/textvectorisation/update/processor/TextToVectorUpdateProcessorTest.java
@@ -17,7 +17,7 @@
 package org.apache.solr.llm.textvectorisation.update.processor;
 
 import java.io.IOException;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.SolrQuery;
 import org.apache.solr.client.solrj.request.UpdateRequest;
@@ -87,7 +87,7 @@ public class TextToVectorUpdateProcessorTest extends TestLlmBase {
     RuntimeException thrown =
         assertThrows(
             "model not found should throw an exception",
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               addWithChain(
                   sdoc("id", "99", "_text_", "Vegeta is the saiyan prince."), "textToVector");
diff --git a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/JSONTupleStream.java b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/JSONTupleStream.java
index e81f4e3709f..ae7b895498f 100644
--- a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/JSONTupleStream.java
+++ b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/JSONTupleStream.java
@@ -141,6 +141,7 @@ public class JSONTupleStream implements TupleStreamParser {
   }
 
   private void handleError() throws IOException {
+    int objectLevel = 0;
     for (; ; ) {
       int event = parser.nextEvent();
       if (event == JSONParser.STRING) {
@@ -154,8 +155,12 @@ public class JSONTupleStream implements TupleStreamParser {
             }
           }
         }
+      } else if (event == JSONParser.OBJECT_START) {
+        objectLevel++;
       } else if (event == JSONParser.OBJECT_END) {
-        throw new IOException("");
+        if (--objectLevel == 0) {
+          throw new IOException("");
+        }
       }
     }
   }
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/RemoteSolrException.java b/solr/solrj/src/java/org/apache/solr/client/solrj/RemoteSolrException.java
new file mode 100644
index 00000000000..94bbca5ebb8
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/RemoteSolrException.java
@@ -0,0 +1,148 @@
+/*
+ * 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.client.solrj;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.common.SolrErrorWrappingException;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.Utils;
+
+/**
+ * Subclass of SolrException that allows us to capture an arbitrary HTTP status code and error
+ * details that may have been returned by the remote server or a proxy along the way.
+ */
+public final class RemoteSolrException extends SolrException {
+
+  private String remoteErrorMessageSuffix = "";
+  private final boolean skipRetry;
+
+  /**
+   * @param remoteHost the host the error was received from
+   * @param code Arbitrary HTTP status code
+   * @param msg Exception Message
+   * @param th Throwable to wrap with this Exception
+   */
+  public RemoteSolrException(String remoteHost, int code, String msg, Throwable th) {
+    super(code, "Error from server at " + remoteHost + ": " + msg, th);
+    skipRetry = false;
+  }
+
+  /**
+   * @param remoteHost the host the error was received from
+   * @param code Arbitrary HTTP status code
+   * @param remoteError Error response sent back from remoteHost
+   */
+  @SuppressWarnings("unchecked")
+  public RemoteSolrException(String remoteHost, int code, Object remoteError) {
+    this(remoteHost, code, remoteError, false);
+  }
+
+  /**
+   * @param remoteHost the host the error was received from
+   * @param code Arbitrary HTTP status code
+   * @param remoteError Error response sent back from remoteHost
+   * @param skipRetry Skip retry if the client is set to retry on failure
+   */
+  @SuppressWarnings("unchecked")
+  public RemoteSolrException(String remoteHost, int code, Object remoteError, boolean skipRetry) {
+    super(ErrorCode.getErrorCode(code), "Error from server at " + remoteHost);
+    this.skipRetry = skipRetry;
+    setDetails(List.of(Map.of("remoteHost", remoteHost, "remoteError", remoteError)));
+    if (remoteError != null) {
+      String remoteErrorMessageSuffix = getRemoteErrorMessageSuffix(remoteError, "");
+      if (remoteErrorMessageSuffix.length() > 2) {
+        this.remoteErrorMessageSuffix = remoteErrorMessageSuffix;
+      }
+
+      Object metadataObj =
+          Utils.getObjectByPath(remoteError, false, Collections.singletonList("metadata"));
+      if (metadataObj instanceof NamedList) {
+        setMetadata((NamedList<String>) metadataObj);
+      } else if (metadataObj instanceof List) {
+        // NamedList parsed as List convert to NamedList again
+        List<Object> list = (List<Object>) metadataObj;
+        setMetadata(new NamedList<>(list.size() / 2));
+        for (int i = 0; i < list.size(); i += 2) {
+          setMetadata((String) list.get(i), (String) list.get(i + 1));
+        }
+      } else if (metadataObj instanceof Map) {
+        setMetadata(new NamedList<>((Map<String, String>) metadataObj));
+      }
+    }
+  }
+
+  private String getRemoteErrorMessageSuffix(Object remoteError, String startingJsonPath) {
+    Object remoteErrorClass =
+        Utils.getObjectByPath(remoteError, false, startingJsonPath + "/errorClass");
+    Object remoteErrorMsg = Utils.getObjectByPath(remoteError, false, startingJsonPath + "/msg");
+    String remoteErrorMessageSuffix = "";
+
+    // Special cases for what the message should look like
+    if (SolrErrorWrappingException.class.getName().equals(remoteErrorClass)) {
+      remoteErrorMessageSuffix =
+          SolrErrorWrappingException.constructMessage(
+              remoteErrorMsg != null ? remoteErrorMsg.toString() : null,
+              Utils.getObjectByPath(remoteError, false, "details"));
+    }
+    if (RemoteSolrException.class.getName().equals(remoteErrorClass)) {
+      remoteErrorMessageSuffix += remoteErrorClass + ": " + remoteErrorMsg;
+      // Drill down into the next remote error to get info from that
+      String nextRemoteErrorMessage =
+          getRemoteErrorMessageSuffix(remoteError, startingJsonPath + "/details[0]/remoteError");
+      if (!nextRemoteErrorMessage.isEmpty()) {
+        remoteErrorMessageSuffix += ": " + nextRemoteErrorMessage;
+      }
+    } else {
+      if (remoteErrorClass != null) {
+        remoteErrorMessageSuffix = remoteErrorMessageSuffix + remoteErrorClass + ": ";
+      }
+      if (remoteErrorMsg != null) {
+        remoteErrorMessageSuffix = remoteErrorMessageSuffix + remoteErrorMsg;
+      }
+    }
+    if (remoteErrorMessageSuffix.isEmpty()) {
+      return "";
+    } else {
+      return ": " + remoteErrorMessageSuffix;
+    }
+  }
+
+  @Override
+  public String getResponseMessage() {
+    return super.getMessage();
+  }
+
+  @Override
+  public String getMessage() {
+    return super.getMessage() + remoteErrorMessageSuffix;
+  }
+
+  public Object getRemoteErrorObject() {
+    if (details != null && !details.isEmpty()) {
+      return details.get(0).get("remoteError");
+    } else {
+      return null;
+    }
+  }
+
+  public boolean shouldSkipRetry() {
+    return skipRetry;
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClient.java
index 76f9888a712..5b21d582d45 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClient.java
@@ -38,7 +38,6 @@ import org.apache.solr.client.solrj.response.StreamingResponseCallback;
 import org.apache.solr.client.solrj.response.UpdateResponse;
 import org.apache.solr.common.SolrDocument;
 import org.apache.solr.common.SolrDocumentList;
-import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
@@ -1214,22 +1213,6 @@ public abstract class SolrClient implements Serializable, Closeable {
     return defaultCollection;
   }
 
-  /**
-   * Subclass of SolrException that allows us to capture an arbitrary HTTP status code that may have
-   * been returned by the remote server or a proxy along the way.
-   */
-  public static class RemoteSolrException extends SolrException {
-    /**
-     * @param remoteHost the host the error was received from
-     * @param code Arbitrary HTTP status code
-     * @param msg Exception Message
-     * @param th Throwable to wrap with this Exception
-     */
-    public RemoteSolrException(String remoteHost, int code, String msg, Throwable th) {
-      super(code, "Error from server at " + remoteHost + ": " + msg, th);
-    }
-  }
-
   /** A lambda intended for invoking SolrClient operations */
   @FunctionalInterface
   public interface SolrClientFunction<C extends SolrClient, R> {
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java
index 23ce0c99b37..c477c55aca4 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.client.solrj.impl;
 
-import static org.apache.solr.client.solrj.SolrClient.RemoteSolrException;
-
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.net.MalformedURLException;
@@ -34,6 +32,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 import org.apache.solr.client.solrj.SolrServerException;
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java
index 38a82b1791e..16bac664891 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/ConcurrentUpdateBaseSolrClient.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -231,7 +232,6 @@ public abstract class ConcurrentUpdateBaseSolrClient extends SolrClient {
     // Pull from the queue multiple times and streams over a single connection.
     // Exits on exception, interruption, or an empty queue to pull from.
     //
-    @SuppressWarnings({"unchecked"})
     void sendUpdateStream() throws Exception {
 
       try {
@@ -255,34 +255,18 @@ public abstract class ConcurrentUpdateBaseSolrClient extends SolrClient {
 
             int statusCode = response.getStatus();
             if (statusCode != HttpStatus.OK_200) {
-              StringBuilder msg = new StringBuilder();
-              msg.append(response.getReason());
-              msg.append("\n\n\n\n");
-              msg.append("request: ").append(basePath);
-
               SolrException solrExc;
-              NamedList<String> metadata = null;
+              Object remoteError = null;
               // parse out the metadata from the SolrException
               try {
                 String encoding = "UTF-8"; // default
                 NamedList<Object> resp = client.getParser().processResponse(rspBody, encoding);
-                NamedList<Object> error = (NamedList<Object>) resp.get("error");
-                if (error != null) {
-                  metadata = (NamedList<String>) error.get("metadata");
-                  String remoteMsg = (String) error.get("msg");
-                  if (remoteMsg != null) {
-                    msg.append("\nRemote error message: ");
-                    msg.append(remoteMsg);
-                  }
-                }
+                remoteError = resp.get("error");
               } catch (Exception exc) {
                 // don't want to fail to report error if parsing the response fails
                 log.warn("Failed to parse error response from {} due to: ", basePath, exc);
               } finally {
-                solrExc = new RemoteSolrException(basePath, statusCode, msg.toString(), null);
-                if (metadata != null) {
-                  solrExc.setMetadata(metadata);
-                }
+                solrExc = new RemoteSolrException(basePath, statusCode, remoteError);
               }
 
               handleError(solrExc);
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java
index e6130255a1f..84e329f25a4 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java
@@ -17,8 +17,6 @@
 
 package org.apache.solr.client.solrj.impl;
 
-import static org.apache.solr.common.util.Utils.getObjectByPath;
-
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -28,13 +26,12 @@ import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -49,7 +46,6 @@ import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.ContentStream;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.Utils;
 
 /**
  * Utility/base functionality for direct HTTP client implementations.
@@ -203,7 +199,7 @@ public abstract class HttpSolrClientBase extends SolrClient {
           break;
         default:
           if (processor == null || mimeType == null) {
-            throw new SolrClient.RemoteSolrException(
+            throw new RemoteSolrException(
                 urlExceptionMessage,
                 httpStatus,
                 "non ok status: " + httpStatus + ", message:" + responseReason,
@@ -224,57 +220,26 @@ public abstract class HttpSolrClientBase extends SolrClient {
       try {
         rsp = processor.processResponse(is, encoding);
       } catch (Exception e) {
-        throw new SolrClient.RemoteSolrException(
-            urlExceptionMessage, httpStatus, e.getMessage(), e);
+        throw new RemoteSolrException(urlExceptionMessage, httpStatus, e.getMessage(), e);
       }
 
       Object error = rsp == null ? null : rsp.get("error");
-
-      if (error != null
-          && (String.valueOf(getObjectByPath(error, true, errPath))
-              .endsWith("ExceptionWithErrObject"))) {
-        throw RemoteExecutionException.create(urlExceptionMessage, rsp);
+      if (error != null && isV2Api) {
+        throw new RemoteSolrException(urlExceptionMessage, httpStatus, error, true);
       }
       if (httpStatus != 200 && !isV2Api) {
-        NamedList<String> metadata = null;
-        String reason = null;
-        try {
-          if (error != null) {
-            reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("msg"));
-            if (reason == null) {
-              reason =
-                  (String) Utils.getObjectByPath(error, false, Collections.singletonList("trace"));
-            }
-            Object metadataObj =
-                Utils.getObjectByPath(error, false, Collections.singletonList("metadata"));
-            if (metadataObj instanceof NamedList) {
-              metadata = (NamedList<String>) metadataObj;
-            } else if (metadataObj instanceof List) {
-              // NamedList parsed as List convert to NamedList again
-              List<Object> list = (List<Object>) metadataObj;
-              metadata = new NamedList<>(list.size() / 2);
-              for (int i = 0; i < list.size(); i += 2) {
-                metadata.add((String) list.get(i), (String) list.get(i + 1));
-              }
-            } else if (metadataObj instanceof Map) {
-              metadata = new NamedList((Map) metadataObj);
-            }
-          }
-        } catch (Exception ex) {
-          /* Ignored */
-        }
-        if (reason == null) {
-          StringBuilder msg = new StringBuilder();
-          msg.append(responseReason).append("\n").append("request: ").append(responseMethod);
-          if (error != null) {
-            msg.append("\n\nError returned:\n").append(error);
-          }
-          reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
+        if (error == null) {
+          StringBuilder msg =
+              new StringBuilder()
+                  .append(responseReason)
+                  .append("\n")
+                  .append("request: ")
+                  .append(responseMethod);
+          String reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
+          throw new RemoteSolrException(urlExceptionMessage, httpStatus, reason, null);
+        } else {
+          throw new RemoteSolrException(urlExceptionMessage, httpStatus, error);
         }
-        SolrClient.RemoteSolrException rss =
-            new SolrClient.RemoteSolrException(urlExceptionMessage, httpStatus, reason, null);
-        if (metadata != null) rss.setMetadata(metadata);
-        throw rss;
       }
       return rsp;
     } finally {
@@ -328,10 +293,10 @@ public abstract class HttpSolrClientBase extends SolrClient {
         try {
           ByteArrayOutputStream body = new ByteArrayOutputStream();
           is.transferTo(body);
-          throw new SolrClient.RemoteSolrException(
+          throw new RemoteSolrException(
               urlExceptionMessage, httpStatus, prefix + body.toString(exceptionEncoding), null);
         } catch (IOException e) {
-          throw new SolrClient.RemoteSolrException(
+          throw new RemoteSolrException(
               urlExceptionMessage,
               httpStatus,
               "Could not parse response with encoding " + exceptionEncoding,
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBAsyncSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBAsyncSolrClient.java
index 41225ffaf25..c02197c2d4d 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBAsyncSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBAsyncSolrClient.java
@@ -22,6 +22,7 @@ import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
@@ -180,9 +181,10 @@ public abstract class LBAsyncSolrClient extends LBSolrClient {
       RetryListener listener) {
     try {
       throw (Exception) oe;
-    } catch (RemoteExecutionException e) {
-      listener.onFailure(e, false);
     } catch (SolrException e) {
+      if (!isNonRetryable && e instanceof RemoteSolrException rse) {
+        isNonRetryable = rse.shouldSkipRetry();
+      }
       // we retry on 404 or 403 or 503 or 500
       // unless it's an update - then we only retry on connect exception
       if (!isNonRetryable && RETRY_CODES.contains(e.code())) {
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
index 48198b1400c..fc0d06c7f29 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
@@ -40,6 +40,7 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
@@ -635,9 +636,10 @@ public abstract class LBSolrClient extends SolrClient {
       if (isZombie) {
         reviveZombieServer(baseUrl);
       }
-    } catch (RemoteExecutionException e) {
-      throw e;
     } catch (SolrException e) {
+      if (!isNonRetryable && e instanceof RemoteSolrException rse) {
+        isNonRetryable = rse.shouldSkipRetry();
+      }
       // we retry on 404 or 403 or 503 or 500
       // unless it's an update - then we only retry on connect exception
       if (!isNonRetryable && RETRY_CODES.contains(e.code())) {
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/RemoteExecutionException.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/RemoteExecutionException.java
deleted file mode 100644
index 18baf4f6756..00000000000
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/RemoteExecutionException.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.client.solrj.impl;
-
-import static org.apache.solr.common.util.Utils.getObjectByPath;
-
-import java.util.Collections;
-import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.common.util.NamedList;
-
-/**
- * This should be thrown when a server has an error in executing the request, and it sends a proper
- * payload back to the client
- *
- * <p>THIS IS INTERNAL AND MAY DISAPPEAR
- */
-public class RemoteExecutionException extends SolrClient.RemoteSolrException {
-  private NamedList<?> meta;
-
-  public RemoteExecutionException(String remoteHost, int code, String msg, NamedList<?> meta) {
-    super(remoteHost, code, msg + (meta != null ? ": " + meta : ""), null);
-    this.meta = meta;
-  }
-
-  public static RemoteExecutionException create(String host, NamedList<?> errResponse) {
-    Object errObj = errResponse.get("error");
-    if (errObj != null) {
-      Number code = (Number) getObjectByPath(errObj, true, Collections.singletonList("code"));
-      String msg = (String) getObjectByPath(errObj, true, Collections.singletonList("msg"));
-      return new RemoteExecutionException(
-          host,
-          code == null ? ErrorCode.UNKNOWN.code : code.intValue(),
-          msg == null ? "Unknown Error" : msg,
-          errResponse);
-
-    } else {
-      throw new RuntimeException("No error");
-    }
-  }
-
-  public NamedList<?> getMetaData() {
-    return meta;
-  }
-}
diff --git a/solr/solrj/src/java/org/apache/solr/common/SolrErrorWrappingException.java b/solr/solrj/src/java/org/apache/solr/common/SolrErrorWrappingException.java
new file mode 100644
index 00000000000..ac7be9227ad
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/common/SolrErrorWrappingException.java
@@ -0,0 +1,56 @@
+/*
+ * 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.common;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.common.util.Utils;
+
+/**
+ * Subclass of SolrException that allows us to wrap arbitrary error objects, and pass them in the
+ * errorMessage while preserving the error object structure when returning a response to the user.
+ */
+public final class SolrErrorWrappingException extends SolrException {
+
+  public SolrErrorWrappingException(ErrorCode code, String msg, List<Map<String, Object>> errs) {
+    super(code, msg);
+    setDetails(errs);
+  }
+
+  @Override
+  public String getResponseMessage() {
+    return super.getMessage();
+  }
+
+  @Override
+  public String getMessage() {
+    return constructMessage(super.getMessage(), getDetails());
+  }
+
+  public static String constructMessage(String message, Object details) {
+    String builtMessage = "";
+    if (message != null) {
+      builtMessage = message;
+    } else {
+      builtMessage = SolrErrorWrappingException.class.getName();
+    }
+    if (details != null) {
+      builtMessage += ", errors: " + Utils.toJSONString(details);
+    }
+    return builtMessage;
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/common/SolrException.java b/solr/solrj/src/java/org/apache/solr/common/SolrException.java
index 983c75bcafd..66a5a4b4fb2 100644
--- a/solr/solrj/src/java/org/apache/solr/common/SolrException.java
+++ b/solr/solrj/src/java/org/apache/solr/common/SolrException.java
@@ -19,8 +19,10 @@ package org.apache.solr.common;
 import static org.apache.solr.client.api.model.ErrorInfo.ERROR_CLASS;
 import static org.apache.solr.client.api.model.ErrorInfo.ROOT_ERROR_CLASS;
 
+import java.util.List;
 import java.util.Map;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
 import org.slf4j.Logger;
 import org.slf4j.MDC;
 
@@ -93,6 +95,7 @@ public class SolrException extends RuntimeException {
 
   int code = 0;
   protected NamedList<String> metadata;
+  protected List<Map<String, Object>> details;
 
   /**
    * The HTTP Status code associated with this Exception. For SolrExceptions thrown by Solr "Server
@@ -122,10 +125,22 @@ public class SolrException extends RuntimeException {
     if (key == null || value == null)
       throw new IllegalArgumentException("Exception metadata cannot be null!");
 
-    if (metadata == null) metadata = new NamedList<>();
+    if (metadata == null) metadata = new SimpleOrderedMap<>();
     metadata.add(key, value);
   }
 
+  public void setDetails(List<Map<String, Object>> details) {
+    this.details = details;
+  }
+
+  public List<Map<String, Object>> getDetails() {
+    return details;
+  }
+
+  public String getResponseMessage() {
+    return getMessage();
+  }
+
   public String getThrowable() {
     return getMetadata(ERROR_CLASS);
   }
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java b/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
index 9cb39b09320..f637d884103 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/JsonTextWriter.java
@@ -291,7 +291,12 @@ public interface JsonTextWriter extends TextWriter {
 
   @Override
   default void writeArray(String name, Iterator<?> val, boolean raw) throws IOException {
-    writeArrayOpener(-1); // no trivial way to determine array size
+    writeArray(name, val, -1, raw);
+  }
+
+  @Override
+  default void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException {
+    writeArrayOpener(size); // no trivial way to determine array size
     writeJsonIter(val, raw);
     writeArrayCloser();
   }
diff --git a/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java b/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
index 5e04815c322..6901b2db5ed 100644
--- a/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
+++ b/solr/solrj/src/java/org/apache/solr/common/util/TextWriter.java
@@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -93,6 +94,8 @@ public interface TextWriter extends PushWriter {
       writeMap(name, ((MapSerializable) val).toMap(new LinkedHashMap<>()), false, true);
     } else if (val instanceof Map) {
       writeMap(name, (Map) val, false, true);
+    } else if (val instanceof Collection<?> cval) { // very generic; keep towards the end
+      writeArray(name, cval.iterator(), cval.size(), raw);
     } else if (val instanceof Iterator) { // very generic; keep towards the end
       writeArray(name, (Iterator) val, raw);
     } else if (val instanceof Iterable) { // very generic; keep towards the end
@@ -129,6 +132,8 @@ public interface TextWriter extends PushWriter {
 
   void writeArray(String name, Iterator<?> val, boolean raw) throws IOException;
 
+  void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException;
+
   void writeNull(String name) throws IOException;
 
   /** if this form of the method is called, val is the Java string form of an int */
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java b/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
index b1b659cb993..8935d0301a5 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/SolrExampleTests.java
@@ -752,14 +752,15 @@ public abstract class SolrExampleTests extends SolrExampleTestsBase {
       assertNotNull("Should throw exception!", concurrentClient.lastError);
       assertEquals(
           "Unexpected exception type",
-          SolrClient.RemoteSolrException.class,
+          RemoteSolrException.class,
           concurrentClient.lastError.getClass());
       assertTrue(
           "Unexpected exception message: " + concurrentClient.lastError.getMessage(),
           concurrentClient
               .lastError
               .getMessage()
-              .contains("Remote error message: Document contains multiple values for uniqueKey"));
+              .contains(
+                  "org.apache.solr.common.SolrException: Document contains multiple values for uniqueKey"));
     } else {
       if (log.isInfoEnabled()) {
         log.info("Ignoring update test for client: {}", client.getClass().getName());
@@ -913,31 +914,26 @@ public abstract class SolrExampleTests extends SolrExampleTestsBase {
 
   @Test
   public void testUpdateRequestWithParameters() throws Exception {
-    SolrClient client = createNewSolrClient();
-
-    client.deleteByQuery("*:*");
-    client.commit();
+    try (SolrClient client = createNewSolrClient()) {
+      client.deleteByQuery("*:*");
+      client.commit();
 
-    SolrInputDocument doc = new SolrInputDocument();
-    doc.addField("id", "id1");
+      SolrInputDocument doc = new SolrInputDocument();
+      doc.addField("id", "id1");
 
-    UpdateRequest req = new UpdateRequest();
-    req.setParam("overwrite", "false");
-    req.add(doc);
-    client.request(req);
-    client.request(req);
-    client.commit();
+      UpdateRequest req = new UpdateRequest();
+      req.setParam("overwrite", "false");
+      req.add(doc);
+      client.request(req);
+      client.request(req);
+      client.commit();
 
-    SolrQuery query = new SolrQuery();
-    query.setQuery("*:*");
-    QueryResponse rsp = client.query(query);
+      SolrQuery query = new SolrQuery();
+      query.setQuery("*:*");
+      QueryResponse rsp = client.query(query);
 
-    SolrDocumentList out = rsp.getResults();
-    assertEquals(2, out.getNumFound());
-    if (!(client instanceof EmbeddedSolrServer)) {
-      /* Do not close in case of using EmbeddedSolrServer,
-       * as that would close the CoreContainer */
-      client.close();
+      SolrDocumentList out = rsp.getResults();
+      assertEquals(2, out.getNumFound());
     }
   }
 
@@ -2442,7 +2438,7 @@ public abstract class SolrExampleTests extends SolrExampleTestsBase {
             "ConcurrentUpdateSolrClient did not report an error", concurrentClient.lastError);
         assertTrue(
             "ConcurrentUpdateSolrClient did not report an error",
-            concurrentClient.lastError.getMessage().contains("Conflict"));
+            concurrentClient.lastError.getMessage().contains("version conflict"));
       } else if (client
           instanceof
           SolrExampleStreamingHttp2Test.ErrorTrackingConcurrentUpdateSolrClient
@@ -2452,7 +2448,7 @@ public abstract class SolrExampleTests extends SolrExampleTestsBase {
             "ConcurrentUpdateSolrClient did not report an error", concurrentClient.lastError);
         assertTrue(
             "ConcurrentUpdateSolrClient did not report an error",
-            concurrentClient.lastError.getMessage().contains("conflict"));
+            concurrentClient.lastError.getMessage().contains("version conflict"));
       }
     } catch (SolrException se) {
       assertTrue(
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/TestSolrJErrorHandling.java b/solr/solrj/src/test/org/apache/solr/client/solrj/TestSolrJErrorHandling.java
index 8c2d5752807..ea3602b5f7d 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/TestSolrJErrorHandling.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/TestSolrJErrorHandling.java
@@ -215,7 +215,7 @@ public class TestSolrJErrorHandling extends SolrJettyTestBase {
   void doSingle(SolrClient client, int threadNum) {
     try {
       client.add(manyDocs(threadNum * 1000000, 1000));
-    } catch (SolrClient.RemoteSolrException e) {
+    } catch (RemoteSolrException e) {
       String msg = e.getMessage();
       assertTrue(msg, msg.contains("field_does_not_exist"));
     } catch (Throwable e) {
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientTest.java
index c6481018de1..95ac8ef41a5 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientTest.java
@@ -38,6 +38,7 @@ import java.util.Set;
 import java.util.concurrent.TimeoutException;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.lucene.tests.util.TestUtil;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -822,7 +823,7 @@ public class CloudHttp2SolrClientTest extends SolrCloudTestCase {
     Replica r = coll.getSlices().iterator().next().getReplicas().iterator().next();
 
     SolrQuery q = new SolrQuery().setQuery("*:*");
-    SolrClient.RemoteSolrException sse = null;
+    RemoteSolrException sse = null;
 
     try (SolrClient solrClient = getHttpSolrClient(r.getBaseUrl(), COLLECTION)) {
 
@@ -876,7 +877,7 @@ public class CloudHttp2SolrClientTest extends SolrCloudTestCase {
       try {
         QueryResponse rsp = solrClient.query(q);
         log.info("error was expected");
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         sse = e;
       }
       assertNotNull(sse);
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java
index 7f5ef6d63fe..4e2bf500e65 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java
@@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import org.apache.lucene.util.NamedThreadFactory;
 import org.apache.solr.client.api.util.SolrVersion;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.JavaBinRequestWriter;
@@ -110,7 +110,7 @@ public class HttpJdkSolrClientTest extends HttpSolrClientTestBase {
     try (HttpJdkSolrClient client = builder(url).build()) {
       try {
         client.deleteById("id");
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       assertEquals("javabin", DebugServlet.parameters.get(CommonParams.WT)[0]);
       validateDelete();
@@ -125,7 +125,7 @@ public class HttpJdkSolrClientTest extends HttpSolrClientTestBase {
         builder(url).withResponseParser(new XMLResponseParser()).build()) {
       try {
         client.deleteByQuery("*:*");
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       assertEquals("xml", DebugServlet.parameters.get(CommonParams.WT)[0]);
       validateDelete();
@@ -225,7 +225,7 @@ public class HttpJdkSolrClientTest extends HttpSolrClientTestBase {
             builder(getBaseUrl() + DEBUG_SERVLET_PATH, DEFAULT_CONNECTION_TIMEOUT, 0).build()) {
       try {
         client.query(q, SolrRequest.METHOD.GET);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
     }
   }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientTestBase.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientTestBase.java
index 632406e3278..c77df8c803f 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientTestBase.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpSolrClientTestBase.java
@@ -36,7 +36,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import org.apache.solr.SolrJettyTestBase;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
@@ -231,22 +231,22 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
     Collection<String> ids = Collections.singletonList("a");
     try {
       client.getById("a");
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
 
     try {
       client.getById(ids, null);
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
 
     try {
       client.getById("foo", "a");
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
 
     try {
       client.getById("foo", ids, null);
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
   }
 
@@ -293,7 +293,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
 
     try {
       client.request(req);
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
 
     assertEquals("post", DebugServlet.lastMethod);
@@ -390,7 +390,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
 
       try {
         client.request(req);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       verifyServletState(client, req);
 
@@ -406,7 +406,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
       setReqParamsOf(req, "requestOnly", "notRequest");
       try {
         client.request(req);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       verifyServletState(client, req);
 
@@ -422,7 +422,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
       setReqParamsOf(req, "serverOnly", "requestOnly", "both", "neither");
       try {
         client.request(req);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       verifyServletState(client, req);
     }
@@ -439,7 +439,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
       setReqParamsOf(req, "serverOnly", "requestOnly", "both", "neither");
       try {
         client.request(req);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       // NOTE: single stream requests send all the params
       // as part of the query string.  So add "neither" to the request,
@@ -651,7 +651,7 @@ public abstract class HttpSolrClientTestBase extends SolrJettyTestBase {
         ee = ee1;
       }
       assertTrue(future.isCompletedExceptionally());
-      assertTrue(ee.getCause() instanceof SolrClient.RemoteSolrException);
+      assertTrue(ee.getCause() instanceof RemoteSolrException);
       assertTrue(ee.getMessage(), ee.getMessage().contains("mime type"));
     }
   }
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java
index 6ce0f910aa9..c213b3d3130 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientCompatibilityTest.java
@@ -19,7 +19,7 @@ package org.apache.solr.client.solrj.jetty;
 
 import org.apache.solr.SolrJettyTestBase;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.SolrQuery;
@@ -59,7 +59,7 @@ public class HttpJettySolrClientCompatibilityTest extends SolrJettyTestBase {
       assertTrue(client.getHttpClient().getTransport() instanceof HttpClientTransportOverHTTP);
       try {
         client.query(new SolrQuery("*:*"), SolrRequest.METHOD.GET);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
     } finally {
       solrClientTestRule.reset();
@@ -80,7 +80,7 @@ public class HttpJettySolrClientCompatibilityTest extends SolrJettyTestBase {
       assertTrue(client.getHttpClient().getTransport() instanceof HttpClientTransportOverHTTP);
       try {
         client.query(new SolrQuery("*:*"), SolrRequest.METHOD.GET);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
     } finally {
       solrClientTestRule.reset();
@@ -105,7 +105,7 @@ public class HttpJettySolrClientCompatibilityTest extends SolrJettyTestBase {
       try {
         client.query(new SolrQuery("*:*"), SolrRequest.METHOD.GET);
         fail("Jetty client with HTTP2 transport should not be able to connect to HTTP1 only nodes");
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
         fail("Jetty client with HTTP2 transport should not be able to connect to HTTP1 only nodes");
       } catch (SolrServerException e) {
         // expected
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java
index 81c9f75bae4..4e927ac7e91 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/jetty/HttpJettySolrClientTest.java
@@ -30,6 +30,7 @@ import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.apache.lucene.tests.util.LuceneTestCase;
 import org.apache.solr.client.api.util.SolrVersion;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -100,7 +101,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
                 .build()) {
       try {
         client.query(q, SolrRequest.METHOD.GET);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
     }
   }
@@ -174,7 +175,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
       client.query(q, method);
       assertEquals(
           client.getParser().getWriterType(), DebugServlet.parameters.get(CommonParams.WT)[0]);
-    } catch (SolrClient.RemoteSolrException ignored) {
+    } catch (RemoteSolrException ignored) {
     }
   }
 
@@ -228,7 +229,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
         new HttpJettySolrClient.Builder(defaultUrl).withDefaultCollection(DEFAULT_CORE).build()) {
       try {
         client.requestWithBaseUrl(urlToUse, (c) -> c.query(queryParams));
-      } catch (SolrClient.RemoteSolrException rse) {
+      } catch (RemoteSolrException rse) {
       }
 
       assertEquals(urlToUse + "/select", DebugServlet.url);
@@ -239,7 +240,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
         new HttpJettySolrClient.Builder(defaultUrl).withDefaultCollection(DEFAULT_CORE).build()) {
       try {
         client.requestWithBaseUrl(urlToUse, new QueryRequest(queryParams), null);
-      } catch (SolrClient.RemoteSolrException rse) {
+      } catch (RemoteSolrException rse) {
       }
 
       assertEquals(urlToUse + "/select", DebugServlet.url);
@@ -254,7 +255,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
         new HttpJettySolrClient.Builder(url).withDefaultCollection(DEFAULT_CORE).build()) {
       try {
         client.deleteById("id");
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       assertEquals("javabin", DebugServlet.parameters.get(CommonParams.WT)[0]);
       validateDelete();
@@ -272,7 +273,7 @@ public class HttpJettySolrClientTest extends HttpSolrClientTestBase {
             .build()) {
       try {
         client.deleteByQuery("*:*");
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       assertEquals("xml", DebugServlet.parameters.get(CommonParams.WT)[0]);
       validateDelete();
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
index d029c54605c..22ea7401ef5 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/SchemaTest.java
@@ -32,6 +32,7 @@ import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import org.apache.commons.io.file.PathUtils;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.request.schema.AnalyzerDefinition;
 import org.apache.solr.client.solrj.request.schema.FieldTypeDefinition;
@@ -62,7 +63,7 @@ public class SchemaTest extends RestTestBase {
 
   private static void assertFailedSchemaResponse(
       ThrowingRunnable runnable, String expectedErrorMessage) {
-    final var e = expectThrows(SolrClient.RemoteSolrException.class, runnable);
+    final var e = expectThrows(RemoteSolrException.class, runnable);
     assertThat(e.getMessage(), containsString(expectedErrorMessage));
   }
 
diff --git a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/ConcurrentUpdateSolrClient.java b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/ConcurrentUpdateSolrClient.java
index 09cdc17b51c..1882dcbcc2f 100644
--- a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/ConcurrentUpdateSolrClient.java
+++ b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/ConcurrentUpdateSolrClient.java
@@ -38,6 +38,7 @@ import org.apache.http.NameValuePair;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ContentProducer;
 import org.apache.http.entity.EntityTemplate;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -219,7 +220,6 @@ public class ConcurrentUpdateSolrClient extends SolrClient {
     // Pull from the queue multiple times and streams over a single connection.
     // Exits on exception, interruption, or an empty queue to pull from.
     //
-    @SuppressWarnings({"unchecked"})
     void sendUpdateStream() throws Exception {
 
       while (!queue.isEmpty()) {
@@ -362,7 +362,7 @@ public class ConcurrentUpdateSolrClient extends SolrClient {
             msg.append("request: ").append(method.getURI());
 
             SolrException solrExc;
-            NamedList<String> metadata = null;
+            Object remoteError = null;
             // parse out the metadata from the SolrException
             try {
               String encoding = "UTF-8"; // default
@@ -378,25 +378,12 @@ public class ConcurrentUpdateSolrClient extends SolrClient {
                 }
               }
               NamedList<Object> resp = client.parser.processResponse(rspBody, encoding);
-              NamedList<Object> error = (NamedList<Object>) resp.get("error");
-              if (error != null) {
-                metadata = (NamedList<String>) error.get("metadata");
-                String remoteMsg = (String) error.get("msg");
-                if (remoteMsg != null) {
-                  msg.append("\nRemote error message: ");
-                  msg.append(remoteMsg);
-                }
-              }
+              remoteError = resp.get("error");
             } catch (Exception exc) {
               // don't want to fail to report error if parsing the response fails
               log.warn("Failed to parse error response from {} due to: ", client.getBaseURL(), exc);
             } finally {
-              solrExc =
-                  new SolrClient.RemoteSolrException(
-                      client.getBaseURL(), statusCode, msg.toString(), null);
-              if (metadata != null) {
-                solrExc.setMetadata(metadata);
-              }
+              solrExc = new RemoteSolrException(client.getBaseURL(), statusCode, remoteError);
             }
 
             handleError(solrExc);
diff --git a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/HttpSolrClient.java b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/HttpSolrClient.java
index 3d11423f440..7f11c01e774 100644
--- a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/HttpSolrClient.java
+++ b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/HttpSolrClient.java
@@ -16,8 +16,6 @@
  */
 package org.apache.solr.client.solrj.apache;
 
-import static org.apache.solr.common.util.Utils.getObjectByPath;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -72,11 +70,11 @@ import org.apache.http.entity.mime.content.StringBody;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.message.BasicNameValuePair;
 import org.apache.solr.client.api.util.SolrVersion;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
-import org.apache.solr.client.solrj.impl.RemoteExecutionException;
 import org.apache.solr.client.solrj.jetty.HttpJettySolrClient;
 import org.apache.solr.client.solrj.request.JavaBinRequestWriter;
 import org.apache.solr.client.solrj.request.RequestWriter;
@@ -92,7 +90,6 @@ import org.apache.solr.common.util.ContentStream;
 import org.apache.solr.common.util.ExecutorUtil;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
-import org.apache.solr.common.util.Utils;
 import org.slf4j.MDC;
 
 /**
@@ -603,7 +600,7 @@ public class HttpSolrClient extends SolrClient {
           break;
         default:
           if (processor == null || contentType == null) {
-            throw new SolrClient.RemoteSolrException(
+            throw new RemoteSolrException(
                 baseUrl,
                 httpStatus,
                 "non ok status: "
@@ -635,7 +632,7 @@ public class HttpSolrClient extends SolrClient {
                 .collect(Collectors.toSet());
         if (!processorMimeTypes.contains(mimeType)) {
           if (isUnmatchedErrorCode(mimeType, httpStatus)) {
-            throw new SolrClient.RemoteSolrException(
+            throw new RemoteSolrException(
                 baseUrl,
                 httpStatus,
                 "non ok status: "
@@ -653,10 +650,10 @@ public class HttpSolrClient extends SolrClient {
           try {
             ByteArrayOutputStream body = new ByteArrayOutputStream();
             respBody.transferTo(body);
-            throw new SolrClient.RemoteSolrException(
+            throw new RemoteSolrException(
                 baseUrl, httpStatus, prefix + body.toString(exceptionCharset), null);
           } catch (IOException e) {
-            throw new SolrClient.RemoteSolrException(
+            throw new RemoteSolrException(
                 baseUrl,
                 httpStatus,
                 "Could not parse response with encoding " + exceptionCharset,
@@ -669,54 +666,24 @@ public class HttpSolrClient extends SolrClient {
       try {
         rsp = processor.processResponse(respBody, charsetName);
       } catch (Exception e) {
-        throw new SolrClient.RemoteSolrException(baseUrl, httpStatus, e.getMessage(), e);
+        throw new RemoteSolrException(baseUrl, httpStatus, e.getMessage(), e);
       }
       Object error = rsp == null ? null : rsp.get("error");
-      if (error != null
-          && (isV2Api
-              || String.valueOf(getObjectByPath(error, true, errPath))
-                  .endsWith("ExceptionWithErrObject"))) {
-        throw RemoteExecutionException.create(baseUrl, rsp);
+      if (error != null && isV2Api) {
+        throw new RemoteSolrException(baseUrl, httpStatus, error, true);
       }
-      if (httpStatus != HttpStatus.SC_OK && !isV2Api) {
-        NamedList<String> metadata = null;
-        String reason = null;
-        try {
-          if (error != null) {
-            reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("msg"));
-            if (reason == null) {
-              reason =
-                  (String) Utils.getObjectByPath(error, false, Collections.singletonList("trace"));
-            }
-            Object metadataObj =
-                Utils.getObjectByPath(error, false, Collections.singletonList("metadata"));
-            if (metadataObj instanceof NamedList) {
-              metadata = (NamedList<String>) metadataObj;
-            } else if (metadataObj instanceof List) {
-              // NamedList parsed as List convert to NamedList again
-              List<Object> list = (List<Object>) metadataObj;
-              metadata = new NamedList<>(list.size() / 2);
-              for (int i = 0; i < list.size(); i += 2) {
-                metadata.add((String) list.get(i), (String) list.get(i + 1));
-              }
-            } else if (metadataObj instanceof Map) {
-              metadata = new NamedList((Map) metadataObj);
-            }
-          }
-        } catch (Exception ex) {
-        }
-        if (reason == null) {
+      if (httpStatus != 200 && !isV2Api) {
+        if (error == null) {
           StringBuilder msg = new StringBuilder();
           msg.append(response.getStatusLine().getReasonPhrase())
               .append("\n\n")
               .append("request: ")
               .append(method.getURI());
-          reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
+          String reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET);
+          throw new RemoteSolrException(baseUrl, httpStatus, reason, null);
+        } else {
+          throw new RemoteSolrException(baseUrl, httpStatus, error);
         }
-        SolrClient.RemoteSolrException rss =
-            new SolrClient.RemoteSolrException(baseUrl, httpStatus, reason, null);
-        if (metadata != null) rss.setMetadata(metadata);
-        throw rss;
       }
       return rsp;
     } catch (ConnectException e) {
diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractCollectionsAPIDistributedZkTestBase.java b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractCollectionsAPIDistributedZkTestBase.java
index e773a941e7d..1fe728e757a 100644
--- a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractCollectionsAPIDistributedZkTestBase.java
+++ b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractCollectionsAPIDistributedZkTestBase.java
@@ -36,6 +36,7 @@ import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.apache.lucene.tests.util.TestUtil;
 import org.apache.solr.client.api.model.CoreStatusResponse;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
@@ -283,7 +284,7 @@ public abstract class AbstractCollectionsAPIDistributedZkTestBase extends SolrCl
             .isSuccess());
 
     expectThrows(
-        SolrClient.RemoteSolrException.class,
+        RemoteSolrException.class,
         () -> {
           CollectionAdminRequest.createCollection("halfcollection", "conf", 1, 1)
               .setCreateNodeSet(nn1 + "," + nn2)
diff --git a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractInstallShardTest.java b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractInstallShardTest.java
index a1aa541959f..eae246c0486 100644
--- a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractInstallShardTest.java
+++ b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractInstallShardTest.java
@@ -35,6 +35,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import org.apache.lucene.store.Directory;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@@ -149,9 +150,9 @@ public abstract class AbstractInstallShardTest extends SolrCloudTestCase {
     deleteAfterTest(collectionName);
 
     final String singleShardLocation = singleShard1Uri.toString();
-    final SolrClient.RemoteSolrException rse =
+    final RemoteSolrException rse =
         expectThrows(
-            SolrClient.RemoteSolrException.class,
+            RemoteSolrException.class,
             () -> {
               CollectionAdminRequest.installDataToShard(
                       collectionName, "shard1", singleShardLocation, BACKUP_REPO_NAME)
@@ -188,7 +189,7 @@ public abstract class AbstractInstallShardTest extends SolrCloudTestCase {
     { // Test synchronous request error reporting
       final var expectedException =
           expectThrows(
-              SolrClient.RemoteSolrException.class,
+              RemoteSolrException.class,
               () -> {
                 CollectionAdminRequest.installDataToShard(
                         collectionName, "shard1", nonExistentLocation, BACKUP_REPO_NAME)
diff --git a/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/BasicHttpSolrClientTest.java b/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/BasicHttpSolrClientTest.java
index e5c987df4cc..c56d452d272 100644
--- a/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/BasicHttpSolrClientTest.java
+++ b/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/BasicHttpSolrClientTest.java
@@ -47,6 +47,7 @@ import org.apache.http.protocol.HttpContext;
 import org.apache.solr.SolrJettyTestBase;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.client.api.util.SolrVersion;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
@@ -143,7 +144,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
     queryRequest.setPath(debugPath);
     try (HttpSolrClient client = getHttpSolrClient(getBaseUrl())) {
 
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       // default method
       assertEquals("get", DebugServlet.lastMethod);
@@ -169,7 +170,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       // POST
       DebugServlet.clear();
       queryRequest.setMethod(METHOD.POST);
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       assertEquals("post", DebugServlet.lastMethod);
       assertEquals(
@@ -190,7 +191,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       // PUT
       DebugServlet.clear();
       queryRequest.setMethod(METHOD.PUT);
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       assertEquals("put", DebugServlet.lastMethod);
       assertEquals(
@@ -217,7 +218,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       // XML/GET
       DebugServlet.clear();
       queryRequest.setMethod(METHOD.GET); // Reset to the default here after using 'PUT' above
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       assertEquals("get", DebugServlet.lastMethod);
       assertEquals(
@@ -235,7 +236,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       // XML/POST
       DebugServlet.clear();
       queryRequest.setMethod(METHOD.POST);
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       assertEquals("post", DebugServlet.lastMethod);
       assertEquals(
@@ -255,7 +256,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
 
       DebugServlet.clear();
       queryRequest.setMethod(METHOD.PUT);
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
 
       assertEquals("put", DebugServlet.lastMethod);
       assertEquals(
@@ -284,7 +285,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       final UpdateRequest deleteById = new UpdateRequest();
       deleteById.deleteById("id");
       deleteById.setPath(debugPath + deleteById.getPath());
-      expectThrows(SolrClient.RemoteSolrException.class, () -> deleteById.process(client));
+      expectThrows(RemoteSolrException.class, () -> deleteById.process(client));
 
       // default method
       assertEquals("post", DebugServlet.lastMethod);
@@ -312,8 +313,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       deleteByQueryRequest.setPath(debugPath + deleteByQueryRequest.getPath());
       deleteByQueryRequest.deleteByQuery("*:*");
       deleteByQueryRequest.setCommitWithin(-1);
-      expectThrows(
-          SolrClient.RemoteSolrException.class, () -> deleteByQueryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> deleteByQueryRequest.process(client));
 
       assertEquals("post", DebugServlet.lastMethod);
       assertEquals(
@@ -333,10 +333,10 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
     DebugServlet.clear();
     try (SolrClient client = getHttpSolrClient(getBaseUrl() + "/debug/foo")) {
       Collection<String> ids = Collections.singletonList("a");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.getById("a"));
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.getById(ids, null));
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.getById("foo", "a"));
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.getById("foo", ids, null));
+      expectThrows(RemoteSolrException.class, () -> client.getById("a"));
+      expectThrows(RemoteSolrException.class, () -> client.getById(ids, null));
+      expectThrows(RemoteSolrException.class, () -> client.getById("foo", "a"));
+      expectThrows(RemoteSolrException.class, () -> client.getById("foo", ids, null));
     }
   }
 
@@ -350,7 +350,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req.add(new SolrInputDocument());
       req.setPath(debugPath + req.getPath());
       req.setParam("a", "\u1234");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> req.process(client));
+      expectThrows(RemoteSolrException.class, () -> req.process(client));
 
       // default method
       assertEquals("post", DebugServlet.lastMethod);
@@ -379,7 +379,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req.setPath(debugPath + req.getPath());
       req.setParam("a", "\u1234");
 
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req));
+      expectThrows(RemoteSolrException.class, () -> client.request(req));
 
       assertEquals("post", DebugServlet.lastMethod);
       assertEquals(
@@ -403,7 +403,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req.setPath(debugPath + req.getPath());
       req.setParam("a", "\u1234");
 
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req));
+      expectThrows(RemoteSolrException.class, () -> client.request(req));
 
       assertEquals("post", DebugServlet.lastMethod);
       assertEquals(
@@ -456,7 +456,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
     try (SolrClient client = getHttpSolrClient(getBaseUrl())) {
       // verify request header gets set
       DebugServlet.clear();
-      expectThrows(SolrClient.RemoteSolrException.class, () -> queryRequest.process(client));
+      expectThrows(RemoteSolrException.class, () -> queryRequest.process(client));
       assertNull(DebugServlet.headers.toString(), DebugServlet.headers.get("accept-encoding"));
     }
 
@@ -464,7 +464,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
         new HttpSolrClient.Builder(getBaseUrl()).allowCompression(true).build()) {
       try {
         queryRequest.process(client);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
       assertNotNull(DebugServlet.headers.get("accept-encoding"));
     }
@@ -473,7 +473,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
         new HttpSolrClient.Builder(getBaseUrl()).allowCompression(false).build()) {
       try {
         queryRequest.process(client);
-      } catch (SolrClient.RemoteSolrException ignored) {
+      } catch (RemoteSolrException ignored) {
       }
     }
     assertNull(DebugServlet.headers.get("accept-encoding"));
@@ -683,7 +683,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       UpdateRequest req = new UpdateRequest();
       req.setPath(debugPath + req.getPath());
       setReqParamsOf(req, "serverOnly", "notServer");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req));
+      expectThrows(RemoteSolrException.class, () -> client.request(req));
       verifyServletState(client, req);
     }
     try (HttpSolrClient client = builder.withTheseParamNamesInTheUrl(Set.of()).build()) {
@@ -693,7 +693,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req2.setPath(debugPath + req2.getPath());
       req2.setQueryParams(Set.of("requestOnly"));
       setReqParamsOf(req2, "requestOnly", "notRequest");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req2));
+      expectThrows(RemoteSolrException.class, () -> client.request(req2));
       verifyServletState(client, req2);
     }
     try (HttpSolrClient client =
@@ -704,7 +704,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req3.setPath(debugPath + req3.getPath());
       req3.setQueryParams(Set.of("requestOnly", "both"));
       setReqParamsOf(req3, "serverOnly", "requestOnly", "both", "neither");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req3));
+      expectThrows(RemoteSolrException.class, () -> client.request(req3));
       verifyServletState(client, req3);
     }
     try (HttpSolrClient client =
@@ -716,7 +716,7 @@ public class BasicHttpSolrClientTest extends SolrJettyTestBase {
       req4.add(new SolrInputDocument());
       req4.setQueryParams(Set.of("requestOnly", "both"));
       setReqParamsOf(req4, "serverOnly", "requestOnly", "both", "neither");
-      expectThrows(SolrClient.RemoteSolrException.class, () -> client.request(req4));
+      expectThrows(RemoteSolrException.class, () -> client.request(req4));
       // NOTE: single stream requests send all the params
       // as part of the query string.  So add "neither" to the request,
       // so it passes the verification step.
diff --git a/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/CloudSolrClientTest.java b/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/CloudSolrClientTest.java
index 8802719304c..89d050dc431 100644
--- a/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/CloudSolrClientTest.java
+++ b/solr/test-framework/src/test/org/apache/solr/client/solrj/apache/CloudSolrClientTest.java
@@ -36,6 +36,7 @@ import java.util.Set;
 import java.util.concurrent.TimeoutException;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.lucene.tests.util.TestUtil;
+import org.apache.solr.client.solrj.RemoteSolrException;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 import org.apache.solr.client.solrj.SolrRequest.SolrRequestType;
@@ -711,7 +712,7 @@ public class CloudSolrClientTest extends SolrCloudTestCase {
     Replica r = coll.getSlices().iterator().next().getReplicas().iterator().next();
 
     SolrQuery q = new SolrQuery().setQuery("*:*");
-    SolrClient.RemoteSolrException sse = null;
+    RemoteSolrException sse = null;
 
     try (SolrClient solrClient = getHttpSolrClient(r.getBaseUrl(), COLLECTION)) {
 
@@ -765,7 +766,7 @@ public class CloudSolrClientTest extends SolrCloudTestCase {
       try {
         QueryResponse rsp = solrClient.query(q);
         log.info("error was expected");
-      } catch (SolrClient.RemoteSolrException e) {
+      } catch (RemoteSolrException e) {
         sse = e;
       }
       assertNotNull(sse);