From 1b6b67617bf3983cb8ec627662bd563df29efeda Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Thu, 1 Oct 2026 17:55:44 +0000 Subject: [PATCH 1/9] SOLR-12849: Resolve alias collection param from POST body too --- changelog/unreleased/SOLR-12849.yml | 7 +++++++ .../src/java/org/apache/solr/servlet/HttpSolrCall.java | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelog/unreleased/SOLR-12849.yml diff --git a/changelog/unreleased/SOLR-12849.yml b/changelog/unreleased/SOLR-12849.yml new file mode 100644 index 000000000000..1da6096af51f --- /dev/null +++ b/changelog/unreleased/SOLR-12849.yml @@ -0,0 +1,7 @@ +title: Resolve alias in collection param from POST body, not just URL query string +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-12849 + url: https://issues.apache.org/jira/browse/SOLR-12849 diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index 35ab076a673d..d601d7447546 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -785,7 +785,9 @@ protected void addCollectionParamIfNeeded(List collections) { return; } assert cores.isZooKeeperAware(); - String collectionParam = queryParams.get(COLLECTION_PROP); + // Read the merged URL + body params: a POST form body may carry the collection param, and it + // needs the same alias-resolution rewrite as a URL query-string param. + String collectionParam = getQueryParams().get(COLLECTION_PROP); // if there is no existing collection param and the core we go to is for the expected // collection, then we needn't add a collection param if (collectionParam == null From d94e216b731ed134a1f0a09d11d74327b6b9fef8 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Thu, 1 Oct 2026 17:55:44 +0000 Subject: [PATCH 2/9] SOLR-12849: add testing handoff --- SOLR-12849-TESTING.md | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 SOLR-12849-TESTING.md diff --git a/SOLR-12849-TESTING.md b/SOLR-12849-TESTING.md new file mode 100644 index 000000000000..8909558116e2 --- /dev/null +++ b/SOLR-12849-TESTING.md @@ -0,0 +1,44 @@ +# SOLR-12849 — Testing Handoff + +> **UNCOMPILED / UNTESTED.** This patch was written against `apache/solr` main +> at `c3e18f1e455` without running a build or any tests. The reviewer (or CI) +> must compile and validate before merge. + +## What changed + +`solr/core/.../servlet/HttpSolrCall.java` — `addCollectionParamIfNeeded()` now +reads the existing `collection` param from the merged URL + body params +(`getQueryParams()`) instead of the URL query string only (`queryParams`). +Previously, a POST form body like `collection=` survived the +alias-resolution rewrite untouched (the early-return saw no URL param), and +the distributed query path then failed with `BAD_REQUEST "Could not find +collection : "` because it resolves the raw param without alias +handling. The identical request as GET worked because the URL param was +rewritten to the resolved collection list. POST now behaves exactly like GET. + +Also added: `changelog/unreleased/SOLR-12849.yml`. + +## Suggested reviewer validation + +```bash +./gradlew :solr:core:compileJava -Pvalidation.errorprone=true +./gradlew :solr:core:test --tests "org.apache.solr.cloud.*Alias*" +``` + +Suggested new coverage (not included): mini-cloud test — create a collection +plus a single-collection alias, then POST `/solr//select` with form +body `q=*:*&collection=` → expect 200 (was 400); mirror with GET to +confirm identical behavior; assert routed docs come from the aliased +collection. Regression checks: no `collection` param, and an already-resolved +comma list, behave as before. + +## Limits / risks + +- A POST body `collection` param pointing at a *different* collection than the + resolved list is now overwritten with the resolved list — matching the + long-standing GET behavior (the ticket's "silently ignored" observation). +- At both call sites (`HttpSolrCall.init`, `V2HttpCall.init`) `solrReq` is + already created, so `getQueryParams()` returns the merged params; if + `solrReq` were null it would fall back to `queryParams`, i.e. the old + behavior. +- No new tests were added in this phase per the contribution workflow. From 2988695849ffa1f6956ed5013665e470cf6c05cc Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 2 Oct 2026 00:05:44 -0600 Subject: [PATCH 3/9] SOLR-12849: remove TESTING.md handoff file --- SOLR-12849-TESTING.md | 44 ------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 SOLR-12849-TESTING.md diff --git a/SOLR-12849-TESTING.md b/SOLR-12849-TESTING.md deleted file mode 100644 index 8909558116e2..000000000000 --- a/SOLR-12849-TESTING.md +++ /dev/null @@ -1,44 +0,0 @@ -# SOLR-12849 — Testing Handoff - -> **UNCOMPILED / UNTESTED.** This patch was written against `apache/solr` main -> at `c3e18f1e455` without running a build or any tests. The reviewer (or CI) -> must compile and validate before merge. - -## What changed - -`solr/core/.../servlet/HttpSolrCall.java` — `addCollectionParamIfNeeded()` now -reads the existing `collection` param from the merged URL + body params -(`getQueryParams()`) instead of the URL query string only (`queryParams`). -Previously, a POST form body like `collection=` survived the -alias-resolution rewrite untouched (the early-return saw no URL param), and -the distributed query path then failed with `BAD_REQUEST "Could not find -collection : "` because it resolves the raw param without alias -handling. The identical request as GET worked because the URL param was -rewritten to the resolved collection list. POST now behaves exactly like GET. - -Also added: `changelog/unreleased/SOLR-12849.yml`. - -## Suggested reviewer validation - -```bash -./gradlew :solr:core:compileJava -Pvalidation.errorprone=true -./gradlew :solr:core:test --tests "org.apache.solr.cloud.*Alias*" -``` - -Suggested new coverage (not included): mini-cloud test — create a collection -plus a single-collection alias, then POST `/solr//select` with form -body `q=*:*&collection=` → expect 200 (was 400); mirror with GET to -confirm identical behavior; assert routed docs come from the aliased -collection. Regression checks: no `collection` param, and an already-resolved -comma list, behave as before. - -## Limits / risks - -- A POST body `collection` param pointing at a *different* collection than the - resolved list is now overwritten with the resolved list — matching the - long-standing GET behavior (the ticket's "silently ignored" observation). -- At both call sites (`HttpSolrCall.init`, `V2HttpCall.init`) `solrReq` is - already created, so `getQueryParams()` returns the merged params; if - `solrReq` were null it would fall back to `queryParams`, i.e. the old - behavior. -- No new tests were added in this phase per the contribution workflow. From f6a16a72feea006dc4096810d988826b8dc6bed8 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 2 Oct 2026 00:20:08 -0600 Subject: [PATCH 4/9] SOLR-12849: add AliasPostBodyTest regression test for alias in POST form body --- .../apache/solr/cloud/AliasPostBodyTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java new file mode 100644 index 000000000000..f4c0119990db --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java @@ -0,0 +1,69 @@ +/* + * 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.cloud; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Verifies that a collection alias in the POST form body is resolved the same as in the URL query + * string (SOLR-12849). + */ +public class AliasPostBodyTest extends SolrCloudTestCase { + + @BeforeClass + public static void setupCluster() throws Exception { + configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure(); + } + + @Test + public void testAliasInPostBody() throws Exception { + String collection = "testcoll"; + String alias = "testalias"; + CollectionAdminRequest.createCollection(collection, "conf", 1, 1) + .processAndWait(cluster.getSolrClient(), 30); + cluster.waitForActiveCollection(collection, 1, 1); + CollectionAdminRequest.createAlias(alias, collection).process(cluster.getSolrClient()); + + // POST to /solr//select with collection= in the FORM BODY (ticket's scenario). + // Must not fail with "Could not find collection". + String baseUrl = cluster.getJettySolrRunners().get(0).getBaseUrl().toString(); + URL url = new URL(baseUrl + "/" + alias + "/select"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + String body = "q=*:*&rows=0&collection=" + alias; + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + int code = conn.getResponseCode(); + String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertEquals(200, code); + assertFalse( + "POST with alias in body failed: " + response, + response.contains("Could not find collection")); + + CollectionAdminRequest.deleteAlias(alias).process(cluster.getSolrClient()); + CollectionAdminRequest.deleteCollection(collection).process(cluster.getSolrClient()); + } +} From 65d929c8a72c271365533d696a332eced967e0be Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 2 Oct 2026 14:22:24 -0600 Subject: [PATCH 5/9] SOLR-12849: Make AliasPostBodyTest deterministic under SSL randomization The suite posts through a raw HttpURLConnection, which cannot validate the randomized test certificate when the suite randomizes SSL on, so the test failed with a TLS error under seeds that enable SSL before it ever reached its assertions. Annotate the suite @SuppressSSL (the scenario is about POST body parameters, not TLS) and build the request URL via URI to satisfy the forbidden APIs check. --- .../src/test/org/apache/solr/cloud/AliasPostBodyTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java index f4c0119990db..81814cf0bf7f 100644 --- a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java @@ -18,8 +18,10 @@ import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import org.apache.solr.SolrTestCaseJ4.SuppressSSL; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.junit.BeforeClass; import org.junit.Test; @@ -28,6 +30,7 @@ * Verifies that a collection alias in the POST form body is resolved the same as in the URL query * string (SOLR-12849). */ +@SuppressSSL // the raw HttpURLConnection used below cannot validate the randomized test cert public class AliasPostBodyTest extends SolrCloudTestCase { @BeforeClass @@ -47,7 +50,7 @@ public void testAliasInPostBody() throws Exception { // POST to /solr//select with collection= in the FORM BODY (ticket's scenario). // Must not fail with "Could not find collection". String baseUrl = cluster.getJettySolrRunners().get(0).getBaseUrl().toString(); - URL url = new URL(baseUrl + "/" + alias + "/select"); + URL url = URI.create(baseUrl + "/" + alias + "/select").toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); From 4eed26de374a17c7ad7b5b76a68ce49eaa283892 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sat, 3 Oct 2026 00:04:32 -0400 Subject: [PATCH 6/9] SOLR-12849: resolve the body collection param on its own instead of replacing it, rewrite the test with SolrJ --- .../org/apache/solr/servlet/HttpSolrCall.java | 15 +++- .../apache/solr/cloud/AliasPostBodyTest.java | 82 +++++++++++-------- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index d601d7447546..ca94bddb9258 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -780,14 +780,21 @@ protected void handleAdmin(SolrQueryResponse solrResp) { * * @see #getCollectionsList() */ - protected void addCollectionParamIfNeeded(List collections) { - if (collections.isEmpty()) { + protected void addCollectionParamIfNeeded(List requestCollections) { + if (requestCollections.isEmpty()) { return; } assert cores.isZooKeeperAware(); - // Read the merged URL + body params: a POST form body may carry the collection param, and it - // needs the same alias-resolution rewrite as a URL query-string param. + // The collections list is computed from the URL params only. A collection param in a POST form + // body is not part of it, so resolve the aliases in that param instead of replacing it. String collectionParam = getQueryParams().get(COLLECTION_PROP); + List collections = requestCollections; + if (collectionParam != null && queryParams.get(COLLECTION_PROP) == null) { + collections = resolveCollectionListOrAlias(collectionParam); + if (collections.isEmpty()) { + return; + } + } // if there is no existing collection param and the core we go to is for the expected // collection, then we needn't add a collection param if (collectionParam == null diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java index 81814cf0bf7f..187c8fc2b687 100644 --- a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java @@ -16,57 +16,67 @@ */ package org.apache.solr.cloud; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import org.apache.solr.SolrTestCaseJ4.SuppressSSL; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.QueryRequest; +import org.apache.solr.client.solrj.request.UpdateRequest; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.params.ModifiableSolrParams; import org.junit.BeforeClass; import org.junit.Test; /** - * Verifies that a collection alias in the POST form body is resolved the same as in the URL query - * string (SOLR-12849). + * Tests that a "collection" parameter in the form body of a POST has its aliases resolved, and is + * otherwise left as it is. */ -@SuppressSSL // the raw HttpURLConnection used below cannot validate the randomized test cert public class AliasPostBodyTest extends SolrCloudTestCase { + private static final String EMPTY_COLLECTION = "emptycoll"; + private static final String DOC_COLLECTION = "doccoll"; + private static final String EMPTY_ALIAS = "emptyalias"; + private static final String DOC_ALIAS = "docalias"; + @BeforeClass public static void setupCluster() throws Exception { configureCluster(1).addConfig("conf", configset("cloud-minimal")).configure(); + + for (String collection : new String[] {EMPTY_COLLECTION, DOC_COLLECTION}) { + CollectionAdminRequest.createCollection(collection, "conf", 1, 1) + .process(cluster.getSolrClient()); + cluster.waitForActiveCollection(collection, 1, 1); + } + CollectionAdminRequest.createAlias(EMPTY_ALIAS, EMPTY_COLLECTION) + .process(cluster.getSolrClient()); + CollectionAdminRequest.createAlias(DOC_ALIAS, DOC_COLLECTION).process(cluster.getSolrClient()); + new UpdateRequest().add("id", "1").commit(cluster.getSolrClient(), DOC_COLLECTION); + } + + /** Sends a form body POST to the path alias, with the collection parameter in the body. */ + private static long postWithCollectionParam(String pathAlias, String collectionParam) + throws Exception { + ModifiableSolrParams params = new ModifiableSolrParams(); + params.set("q", "*:*"); + params.set("rows", "0"); + params.set("collection", collectionParam); + SolrClient nodeClient = cluster.getJettySolrRunner(0).getSolrClient(); + QueryResponse response = + new QueryRequest(params, SolrRequest.METHOD.POST).process(nodeClient, pathAlias); + return response.getResults().getNumFound(); } @Test - public void testAliasInPostBody() throws Exception { - String collection = "testcoll"; - String alias = "testalias"; - CollectionAdminRequest.createCollection(collection, "conf", 1, 1) - .processAndWait(cluster.getSolrClient(), 30); - cluster.waitForActiveCollection(collection, 1, 1); - CollectionAdminRequest.createAlias(alias, collection).process(cluster.getSolrClient()); + public void testAliasInPostBodyIsResolved() throws Exception { + assertEquals(0, postWithCollectionParam(EMPTY_ALIAS, EMPTY_ALIAS)); + } - // POST to /solr//select with collection= in the FORM BODY (ticket's scenario). - // Must not fail with "Could not find collection". - String baseUrl = cluster.getJettySolrRunners().get(0).getBaseUrl().toString(); - URL url = URI.create(baseUrl + "/" + alias + "/select").toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setDoOutput(true); - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - String body = "q=*:*&rows=0&collection=" + alias; - try (OutputStream os = conn.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); - } - int code = conn.getResponseCode(); - String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - assertEquals(200, code); - assertFalse( - "POST with alias in body failed: " + response, - response.contains("Could not find collection")); + @Test + public void testAliasInPostBodyDiffersFromThePathAlias() throws Exception { + assertEquals(1, postWithCollectionParam(EMPTY_ALIAS, DOC_ALIAS)); + } - CollectionAdminRequest.deleteAlias(alias).process(cluster.getSolrClient()); - CollectionAdminRequest.deleteCollection(collection).process(cluster.getSolrClient()); + @Test + public void testCollectionInPostBodyIsNotReplacedByThePathCollection() throws Exception { + assertEquals(1, postWithCollectionParam(EMPTY_ALIAS, DOC_COLLECTION)); } } From d0e486e39f045bc5c38bce4687f0373c888f1b36 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 4 Oct 2026 21:56:31 +0000 Subject: [PATCH 7/9] SOLR-12849: add a two-collection alias case and a method-level test for the body collection parameter --- .../apache/solr/cloud/AliasPostBodyTest.java | 12 ++++ .../HttpSolrCallCollectionParamTest.java | 72 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 solr/core/src/test/org/apache/solr/servlet/HttpSolrCallCollectionParamTest.java diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java index 187c8fc2b687..09802253d5e2 100644 --- a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java @@ -36,6 +36,7 @@ public class AliasPostBodyTest extends SolrCloudTestCase { private static final String DOC_COLLECTION = "doccoll"; private static final String EMPTY_ALIAS = "emptyalias"; private static final String DOC_ALIAS = "docalias"; + private static final String BOTH_ALIAS = "bothalias"; @BeforeClass public static void setupCluster() throws Exception { @@ -49,6 +50,8 @@ public static void setupCluster() throws Exception { CollectionAdminRequest.createAlias(EMPTY_ALIAS, EMPTY_COLLECTION) .process(cluster.getSolrClient()); CollectionAdminRequest.createAlias(DOC_ALIAS, DOC_COLLECTION).process(cluster.getSolrClient()); + CollectionAdminRequest.createAlias(BOTH_ALIAS, EMPTY_COLLECTION + "," + DOC_COLLECTION) + .process(cluster.getSolrClient()); new UpdateRequest().add("id", "1").commit(cluster.getSolrClient(), DOC_COLLECTION); } @@ -79,4 +82,13 @@ public void testAliasInPostBodyDiffersFromThePathAlias() throws Exception { public void testCollectionInPostBodyIsNotReplacedByThePathCollection() throws Exception { assertEquals(1, postWithCollectionParam(EMPTY_ALIAS, DOC_COLLECTION)); } + + @Test + public void testBodyCollectionSurvivesTwoCollectionPathAlias() throws Exception { + // The path alias resolves to two collections, so routing derives a two-collection + // list for the request. The body's collection (the empty one) must still win: on + // the base code it is overwritten with the path list and the document in the other + // collection is counted too. + assertEquals(0, postWithCollectionParam(BOTH_ALIAS, EMPTY_COLLECTION)); + } } diff --git a/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallCollectionParamTest.java b/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallCollectionParamTest.java new file mode 100644 index 000000000000..a4a0a9ba1eca --- /dev/null +++ b/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallCollectionParamTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.servlet; + +import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito; +import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import org.apache.solr.SolrTestCase; +import org.apache.solr.common.cloud.Aliases; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.request.SolrQueryRequestBase; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Method-level test for {@link HttpSolrCall#addCollectionParamIfNeeded(List)}: a {@code collection} + * parameter supplied in a POST form body must not be replaced by the list of collections derived + * from the request path when that list holds more than one collection. + */ +public class HttpSolrCallCollectionParamTest extends SolrTestCase { + + @BeforeClass + public static void setUpOnce() { + assumeWorkingMockito(); + } + + @Test + public void testAddCollectionParamIfNeededKeepsBodyValueForTwoCollectionPath() { + CoreContainer cores = mock(CoreContainer.class); + when(cores.isZooKeeperAware()).thenReturn(true); + when(cores.getAliases()).thenReturn(Aliases.EMPTY); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getServletPath()).thenReturn("/bothalias"); + when(request.getPathInfo()).thenReturn("/select"); + HttpServletResponse response = mock(HttpServletResponse.class); + + HttpSolrCall call = new HttpSolrCall(cores, request, response, false); + // The URL query string carries no collection parameter... + call.queryParams = SolrRequestParsers.parseQueryString("q=*:*"); + // ...but the parsed request, which merges in the POST form body, carries one. + ModifiableSolrParams requestParams = new ModifiableSolrParams(); + requestParams.set(COLLECTION_PROP, "bodycoll"); + requestParams.set("q", "*:*"); + call.solrReq = new SolrQueryRequestBase(null, requestParams); + + // The request path names an alias that resolves to two collections. + call.addCollectionParamIfNeeded(List.of("emptycollection", "doccollection")); + + assertEquals("bodycoll", call.solrReq.getParams().get(COLLECTION_PROP)); + } +} From 6b3a2829fe54540ff835405726cdecda71655426 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 4 Oct 2026 23:56:43 +0000 Subject: [PATCH 8/9] SOLR-12849: correct the AliasPostBodyTest comment to match the verified base behavior --- .../test/org/apache/solr/cloud/AliasPostBodyTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java index 09802253d5e2..b597d508cd66 100644 --- a/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/AliasPostBodyTest.java @@ -85,10 +85,12 @@ public void testCollectionInPostBodyIsNotReplacedByThePathCollection() throws Ex @Test public void testBodyCollectionSurvivesTwoCollectionPathAlias() throws Exception { - // The path alias resolves to two collections, so routing derives a two-collection - // list for the request. The body's collection (the empty one) must still win: on - // the base code it is overwritten with the path list and the document in the other - // collection is counted too. + // The path alias resolves to two collections, but the collection named by the + // request must still win. A body-only collection value is replaced with the joined + // path list inside addCollectionParamIfNeeded on the base code (see + // HttpSolrCallCollectionParamTest); this end-to-end case passes on base as well, + // because the SolrJ client also places the collection parameter in the URL, where + // it takes precedence over the path. assertEquals(0, postWithCollectionParam(BOTH_ALIAS, EMPTY_COLLECTION)); } } From cd7bd799c91bb1f40a37ea5fe398411ccc9bd1b6 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 4 Oct 2026 23:56:43 +0000 Subject: [PATCH 9/9] SOLR-12849: update the changelog title to match the narrowed PR title --- changelog/unreleased/SOLR-12849.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/unreleased/SOLR-12849.yml b/changelog/unreleased/SOLR-12849.yml index 1da6096af51f..4fec07ae30d8 100644 --- a/changelog/unreleased/SOLR-12849.yml +++ b/changelog/unreleased/SOLR-12849.yml @@ -1,4 +1,4 @@ -title: Resolve alias in collection param from POST body, not just URL query string +title: Keep a POST body's collection parameter when the path names multiple collections type: fixed authors: - name: Nick Shanin