This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit d5fd1659a62853a7443569a069de6843e2a3f01c 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) (cherry picked from commit 74197140a666d4f0396b68484c2bf9df7e79221e) --- .../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 283df3e26ed..d88534352e8 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 @@ -1489,8 +1489,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; + } +}
