From bc37b68e292c1434e032b69fa9dea97b662ce4c0 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 17 Oct 2026 10:01:01 -0400 Subject: [PATCH 1/3] Fix v2 API 404 when a /config request is proxied to another node Solr forwards a v2 request to the node that actually hosts the target collection using the short "/c/{collection}/..." path alias (see HttpSolrCall#sendRemoteProxy), but Constants.INDEX_PATH_PREFIX's JAX-RS @Path regex only recognized "cores" or "collections", not "c". Any JAX-RS resource built on that prefix therefore 404'd with "Cannot find API for the path" whenever a request needed to be proxied cross-node. Reproduced with a new 2-node test in GetConfigSolrCloudTest that deliberately sends a GET /config/overlay request to the node that does NOT host the collection's only replica, then fixed by accepting "c" as an alias for "collections" in the path regex. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RzzUgiebQ8ZhhgE2E7ZZU5 --- .../solr/client/api/util/Constants.java | 12 ++++++- .../admin/api/GetConfigSolrCloudTest.java | 34 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/solr/api/src/java/org/apache/solr/client/api/util/Constants.java b/solr/api/src/java/org/apache/solr/client/api/util/Constants.java index 3e3c7443c18f..13f8009fbb41 100644 --- a/solr/api/src/java/org/apache/solr/client/api/util/Constants.java +++ b/solr/api/src/java/org/apache/solr/client/api/util/Constants.java @@ -24,8 +24,18 @@ private Constants() { public static final String INDEX_TYPE_PATH_PARAMETER = "indexType"; public static final String INDEX_NAME_PATH_PARAMETER = "indexName"; + + // "c" is accepted alongside "collections" because Solr internally forwards a v2 request to the + // node actually hosting a collection using the short "/c/{collection}/..." alias (see + // HttpSolrCall#sendRemoteProxy and V2HttpCall's "c"/"collections" handling); without it, any + // JAX-RS resource built on this prefix 404s with "Cannot find API for the path" whenever the + // request has to be proxied to another node. public static final String INDEX_PATH_PREFIX = - "/{" + INDEX_TYPE_PATH_PARAMETER + ":cores|collections}/{" + INDEX_NAME_PATH_PARAMETER + "}"; + "/{" + + INDEX_TYPE_PATH_PARAMETER + + ":cores|collections|c}/{" + + INDEX_NAME_PATH_PARAMETER + + "}"; public static final String CORE_NAME_PATH_PARAMETER = "coreName"; diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java index 19b29380afd8..8a23c749e1db 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java @@ -19,9 +19,12 @@ import static org.apache.solr.core.CoreContainer.ALLOW_PATHS_SYSPROP; import org.apache.solr.client.api.model.IndexType; +import org.apache.solr.client.solrj.jetty.HttpJettySolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.request.ConfigApi; import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.cloud.Replica; +import org.apache.solr.embedded.JettySolrRunner; import org.apache.solr.util.ExternalPaths; import org.junit.BeforeClass; import org.junit.Test; @@ -34,7 +37,9 @@ public class GetConfigSolrCloudTest extends SolrCloudTestCase { @BeforeClass public static void setupCluster() throws Exception { System.setProperty(ALLOW_PATHS_SYSPROP, ExternalPaths.SERVER_HOME.toAbsolutePath().toString()); - configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure(); + // 2 nodes so that a request can land on a node that does *not* host the collection's only + // replica, forcing Solr to internally proxy the request to the node that does. + configureCluster(2).addConfig("conf", configset("cloud-minimal")).configure(); CollectionAdminRequest.createCollection(COLLECTION_NAME, "conf", 1, 1) .process(cluster.getSolrClient()); } @@ -77,6 +82,33 @@ public void testGetZnodeVersionFromCollection() throws Exception { assertTrue(response.znodeVersion.params >= -1); } + /** + * Reproduces a bug where a v2 "/config" request proxied internally to the node actually hosting + * the collection fails with "Cannot find API for the path", because Solr forwards such requests + * using the short "/c/{collection}/..." alias, but {@code Constants.INDEX_PATH_PREFIX}'s JAX-RS + * {@code @Path} regex only recognizes "cores" or "collections", not "c". + */ + @Test + public void testGetOverlayFromCollectionViaNonHostingNode() throws Exception { + Replica replica = getCollectionState(COLLECTION_NAME).replicaStream().findFirst().orElseThrow(); + JettySolrRunner hostingJetty = cluster.getReplicaJetty(replica); + JettySolrRunner otherJetty = + cluster.getJettySolrRunners().stream() + .filter(jetty -> jetty != hostingJetty) + .findFirst() + .orElseThrow(); + + try (var client = new HttpJettySolrClient.Builder(otherJetty.getBaseUrl().toString()).build()) { + var request = new ConfigApi.GetOverlay(IndexType.COLLECTION, COLLECTION_NAME); + var response = request.process(client); + + assertNotNull(response); + assertNull(response.error); + assertNotNull(response.overlay); + assertTrue(response.overlay.containsKey("znodeVersion")); + } + } + @Test public void testGetConfigComponentFromCollection() throws Exception { for (String component : GetConfig.MIGRATED_CONFIG_COMPONENTS) { From 5b168595944fb182ecd2a67716e53ce4fce23c86 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 17 Oct 2026 11:13:33 -0400 Subject: [PATCH 2/3] Fix ErrorProne ReferenceEquality failure in the new regression test CI builds with -Werror, and errorprone flags comparing JettySolrRunner instances with != instead of .equals(). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RzzUgiebQ8ZhhgE2E7ZZU5 --- .../apache/solr/handler/admin/api/GetConfigSolrCloudTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java index 8a23c749e1db..51ff1504293d 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java @@ -94,7 +94,7 @@ public void testGetOverlayFromCollectionViaNonHostingNode() throws Exception { JettySolrRunner hostingJetty = cluster.getReplicaJetty(replica); JettySolrRunner otherJetty = cluster.getJettySolrRunners().stream() - .filter(jetty -> jetty != hostingJetty) + .filter(jetty -> !jetty.equals(hostingJetty)) .findFirst() .orElseThrow(); From 3e6817c4f617f06efaca995a3e583a8ad400a8e3 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 17 Oct 2026 18:08:32 -0400 Subject: [PATCH 3/3] Drop over-explaining javadoc on the regression test The method name already says what the test does; per dsmiley's review comment, the javadoc was redundant. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01RzzUgiebQ8ZhhgE2E7ZZU5 --- .../solr/handler/admin/api/GetConfigSolrCloudTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java index 51ff1504293d..dd51d21ddeaf 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/GetConfigSolrCloudTest.java @@ -82,12 +82,6 @@ public void testGetZnodeVersionFromCollection() throws Exception { assertTrue(response.znodeVersion.params >= -1); } - /** - * Reproduces a bug where a v2 "/config" request proxied internally to the node actually hosting - * the collection fails with "Cannot find API for the path", because Solr forwards such requests - * using the short "/c/{collection}/..." alias, but {@code Constants.INDEX_PATH_PREFIX}'s JAX-RS - * {@code @Path} regex only recognizes "cores" or "collections", not "c". - */ @Test public void testGetOverlayFromCollectionViaNonHostingNode() throws Exception { Replica replica = getCollectionState(COLLECTION_NAME).replicaStream().findFirst().orElseThrow();