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

tflobbe 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 74197140a66 SOLR-18596: NullPointerException in 
QueryComponent.returnFields on a cause-less SolrServerException (#5249)
74197140a66 is described below

commit 74197140a666d4f0396b68484c2bf9df7e79221e
Author: Julian Chandra Sutadi <[email protected]>
AuthorDate: Wed Oct 28 20:54:05 2026 +0700

    SOLR-18596: NullPointerException in QueryComponent.returnFields on a 
cause-less SolrServerException (#5249)
---
 .../unreleased/SOLR-18596-returnfields-npe.yml     |   8 ++
 .../solr/handler/component/QueryComponent.java     |   4 +-
 .../QueryComponentReturnFieldsNpeTest.java         | 127 +++++++++++++++++++++
 3 files changed, 137 insertions(+), 2 deletions(-)

diff --git a/changelog/unreleased/SOLR-18596-returnfields-npe.yml 
b/changelog/unreleased/SOLR-18596-returnfields-npe.yml
new file mode 100644
index 00000000000..47adacae9bb
--- /dev/null
+++ b/changelog/unreleased/SOLR-18596-returnfields-npe.yml
@@ -0,0 +1,8 @@
+title: Distributed search with shards.info=true no longer throws a 
NullPointerException when a shard fails with a cause-less SolrServerException
+type: fixed
+authors:
+  - name: Julian Chandra Sutadi
+    nick: julianchandras
+links:
+  - name: SOLR-18596
+    url: https://issues.apache.org/jira/browse/SOLR-18596
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java 
b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index c3cd238ee5d..5c8c58ed3ff 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -1484,8 +1484,8 @@ public class QueryComponent extends SearchComponent {
             if (nl.get("error") == null) {
               // Add the error to the shards info section if it wasn't added 
before
               Throwable t = srsp.getException();
-              if (t instanceof SolrServerException) {
-                t = ((SolrServerException) t).getCause();
+              if (t instanceof SolrServerException && t.getCause() != null) {
+                t = t.getCause();
               }
               nl.add("error", t.toString());
               if (!rb.req.getCore().getCoreContainer().hideStackTrace()) {
diff --git 
a/solr/core/src/test/org/apache/solr/handler/component/QueryComponentReturnFieldsNpeTest.java
 
b/solr/core/src/test/org/apache/solr/handler/component/QueryComponentReturnFieldsNpeTest.java
new file mode 100644
index 00000000000..ee21a57e0f1
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/component/QueryComponentReturnFieldsNpeTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.handler.component;
+
+import java.util.ArrayList;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.StrField;
+import org.apache.solr.search.SolrReturnFields;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class QueryComponentReturnFieldsNpeTest extends SolrTestCaseJ4 {
+
+  // LBSolrClient's cause-less exception message
+  private static final String EXCEPTION_MESSAGE = "Time allowed to handle this 
request exceeded";
+
+  private static final String EXPECTED_ERROR =
+      "org.apache.solr.client.solrj.SolrServerException: " + EXCEPTION_MESSAGE;
+
+  // SOLR-18596: a cause-less SolrServerException must be recorded, not cause 
an NPE.
+  @Test
+  public void recordsCauselessSolrServerException() {
+    assumeWorkingMockito();
+    SimpleOrderedMap<Object> shardEntry = new SimpleOrderedMap<>();
+    ResponseBuilder rb = getFieldsResponseBuilder(shardEntry, true);
+    ShardRequest sreq = getFieldsRequest(new 
SolrServerException(EXCEPTION_MESSAGE));
+
+    new QueryComponent().returnFields(rb, sreq);
+
+    assertEquals(EXPECTED_ERROR, shardEntry.get("error"));
+  }
+
+  // When the SolrServerException has a cause, it is unwrapped to that cause.
+  @Test
+  public void unwrapsCauseWhenPresent() {
+    assumeWorkingMockito();
+    SimpleOrderedMap<Object> shardEntry = new SimpleOrderedMap<>();
+    ResponseBuilder rb = getFieldsResponseBuilder(shardEntry, true);
+    ShardRequest sreq =
+        getFieldsRequest(new SolrServerException("wrapper", new 
RuntimeException("root cause")));
+
+    new QueryComponent().returnFields(rb, sreq);
+
+    assertEquals("java.lang.RuntimeException: root cause", 
shardEntry.get("error"));
+  }
+
+  // When the stack trace is not hidden, the trace branch records a trace 
alongside the error.
+  @Test
+  public void recordsTraceWhenStackTraceVisible() {
+    assumeWorkingMockito();
+    SimpleOrderedMap<Object> shardEntry = new SimpleOrderedMap<>();
+    ResponseBuilder rb = getFieldsResponseBuilder(shardEntry, false);
+    ShardRequest sreq = getFieldsRequest(new 
SolrServerException(EXCEPTION_MESSAGE));
+
+    new QueryComponent().returnFields(rb, sreq);
+
+    assertEquals(EXPECTED_ERROR, shardEntry.get("error"));
+    Object trace = shardEntry.get("trace");
+    assertNotNull("stack trace should be recorded when not hidden", trace);
+    assertTrue(
+        "trace should contain the exception", 
trace.toString().contains("SolrServerException"));
+  }
+
+  private static ResponseBuilder getFieldsResponseBuilder(
+      SimpleOrderedMap<Object> shardEntry, boolean hideStackTrace) {
+    // Request: unique key "id", shards.info=true.
+    SolrQueryRequest req = Mockito.mock(SolrQueryRequest.class);
+    IndexSchema schema = Mockito.mock(IndexSchema.class);
+    Mockito.when(req.getSchema()).thenReturn(schema);
+    Mockito.when(schema.getUniqueKeyField()).thenReturn(new SchemaField("id", 
new StrField()));
+    ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set(ShardParams.SHARDS_INFO, true);
+    Mockito.when(req.getParams()).thenReturn(params);
+    SolrCore core = Mockito.mock(SolrCore.class);
+    CoreContainer cc = Mockito.mock(CoreContainer.class);
+    Mockito.when(req.getCore()).thenReturn(core);
+    Mockito.when(core.getCoreContainer()).thenReturn(cc);
+    Mockito.when(cc.hideStackTrace()).thenReturn(hideStackTrace);
+
+    // Response: real ReturnFields + a values list already holding a 
shards.info entry for shard1.
+    SolrQueryResponse rsp = Mockito.mock(SolrQueryResponse.class);
+    Mockito.when(rsp.getReturnFields()).thenReturn(new SolrReturnFields());
+    NamedList<Object> values = new NamedList<>();
+    SimpleOrderedMap<Object> shardsInfo = new SimpleOrderedMap<>();
+    shardsInfo.add("shard1", shardEntry);
+    values.add(ShardParams.SHARDS_INFO, shardsInfo);
+    Mockito.when(rsp.getValues()).thenReturn(values);
+
+    return new ResponseBuilder(req, rsp, new ArrayList<>());
+  }
+
+  // A GET_FIELDS shard request whose shard failed with the given exception.
+  private static ShardRequest getFieldsRequest(Throwable exception) {
+    ShardRequest sreq = new ShardRequest();
+    sreq.purpose = ShardRequest.PURPOSE_GET_FIELDS;
+    ShardResponse srsp = new ShardResponse();
+    srsp.setShard("shard1");
+    srsp.setException(exception);
+    sreq.responses.add(srsp);
+    return sreq;
+  }
+}

Reply via email to