This is an automated email from the ASF dual-hosted git repository.
mkhludnev 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 93e357bcf6b SOLR-13202: Return 400 instead of 500 for join queries
missing from/to parameters (#5012)
93e357bcf6b is described below
commit 93e357bcf6bc9b3b6195043529ef5e1fa2001fde
Author: Nick <[email protected]>
AuthorDate: Wed Oct 7 09:03:48 2026 -0600
SOLR-13202: Return 400 instead of 500 for join queries missing from/to
parameters (#5012)
---
changelog/unreleased/SOLR-13202.yml | 7 +++++++
.../org/apache/solr/search/JoinQParserPlugin.java | 2 ++
.../search/join/AuxIndexJoinQParserPlugin.java | 4 +---
.../search/join/CrossCollectionJoinQParser.java | 1 +
.../solr/search/join/ScoreJoinQParserPlugin.java | 13 ++++++++++++
solr/core/src/test/org/apache/solr/TestJoin.java | 15 ++++++++++++++
.../search/join/CrossCollectionJoinQueryTest.java | 23 ++++++++++++++++++++++
.../search/join/TestAuxIndexJoinQParserPlugin.java | 11 +++++++----
.../search/join/TestGlobalOrdinalsJoinQParser.java | 13 ++++++++++++
9 files changed, 82 insertions(+), 7 deletions(-)
diff --git a/changelog/unreleased/SOLR-13202.yml
b/changelog/unreleased/SOLR-13202.yml
new file mode 100644
index 00000000000..1d4ea052cec
--- /dev/null
+++ b/changelog/unreleased/SOLR-13202.yml
@@ -0,0 +1,7 @@
+title: Return 400 instead of 500 for join queries, including cross-collection
joins, that are missing the from or to parameter
+type: fixed
+authors:
+ - name: Nick Shanin
+links:
+ - name: SOLR-13202
+ url: https://issues.apache.org/jira/browse/SOLR-13202
diff --git a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
index b403c0c92a2..343210cde7f 100644
--- a/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/JoinQParserPlugin.java
@@ -146,6 +146,8 @@ public class JoinQParserPlugin extends QParserPlugin {
final String v = qparser.localParams.get(QueryParsing.V);
final String coreName;
+ ScoreJoinQParserPlugin.requireFromAndTo(fromField, toField);
+
Query fromQuery;
long fromCoreOpenTime = 0;
diff --git
a/solr/core/src/java/org/apache/solr/search/join/AuxIndexJoinQParserPlugin.java
b/solr/core/src/java/org/apache/solr/search/join/AuxIndexJoinQParserPlugin.java
index 9535dcf5226..0f7cfee6317 100644
---
a/solr/core/src/java/org/apache/solr/search/join/AuxIndexJoinQParserPlugin.java
+++
b/solr/core/src/java/org/apache/solr/search/join/AuxIndexJoinQParserPlugin.java
@@ -408,9 +408,7 @@ public class AuxIndexJoinQParserPlugin extends QParserPlugin
}
final String fromField = getParam("from");
final String toField = getParam("to");
- if (fromField == null || toField == null) {
- throw new SyntaxError("auxIndexJoin query parser requires 'from' and
'to' local params");
- }
+ ScoreJoinQParserPlugin.requireFromAndTo(fromField, toField);
final String fromIndex = localParams.get("fromIndex");
final String v = localParams.get(CommonParams.VALUE);
final String myCore = req.getCore().getCoreDescriptor().getName();
diff --git
a/solr/core/src/java/org/apache/solr/search/join/CrossCollectionJoinQParser.java
b/solr/core/src/java/org/apache/solr/search/join/CrossCollectionJoinQParser.java
index 28b08154f57..4581facf15d 100644
---
a/solr/core/src/java/org/apache/solr/search/join/CrossCollectionJoinQParser.java
+++
b/solr/core/src/java/org/apache/solr/search/join/CrossCollectionJoinQParser.java
@@ -106,6 +106,7 @@ public class CrossCollectionJoinQParser extends QParser {
String collection = localParams.get(FROM_INDEX);
String fromField = localParams.get(FROM);
String toField = localParams.get(TO);
+ ScoreJoinQParserPlugin.requireFromAndTo(fromField, toField);
boolean routedByJoinKey = localParams.getBool(ROUTED_BY_JOIN_KEY,
toField.equals(routerField));
int ttl = localParams.getInt(TTL, TTL_DEFAULT);
diff --git
a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
index 29b4a44f432..08beafdc190 100644
--- a/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/join/ScoreJoinQParserPlugin.java
@@ -355,6 +355,18 @@ public class ScoreJoinQParserPlugin extends QParserPlugin {
}
}
+ /** Fails with a bad request if a join has no {@code from} or no {@code to}
field. */
+ public static void requireFromAndTo(String fromField, String toField) {
+ if (fromField == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST, "Join query missing required
'from' parameter");
+ }
+ if (toField == null) {
+ throw new SolrException(
+ SolrException.ErrorCode.BAD_REQUEST, "Join query missing required
'to' parameter");
+ }
+ }
+
@Override
public QParser createParser(
String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest
req) {
@@ -364,6 +376,7 @@ public class ScoreJoinQParserPlugin extends QParserPlugin {
final String fromField = localParams.get("from");
final String fromIndex = localParams.get("fromIndex");
final String toField = localParams.get("to");
+ requireFromAndTo(fromField, toField);
final ScoreMode scoreMode = ScoreModeParser.parse(getParam(SCORE));
final String v = localParams.get(CommonParams.VALUE);
diff --git a/solr/core/src/test/org/apache/solr/TestJoin.java
b/solr/core/src/test/org/apache/solr/TestJoin.java
index 14f776610ba..85e4b4f128b 100644
--- a/solr/core/src/test/org/apache/solr/TestJoin.java
+++ b/solr/core/src/test/org/apache/solr/TestJoin.java
@@ -490,4 +490,19 @@ public class TestJoin extends SolrTestCaseJ4 {
return "{!join " + allProvidedParams + " method=topLevelDV}";
}
}
+
+ @Test
+ public void testJoinMissingFromToReturns400() {
+ for (String method :
+ List.of("", "method=dvWithScore score=none", "method=topLevelDV",
"score=none")) {
+ assertMissingJoinParam("{!join to=id " + method + "}*:*", "from");
+ assertMissingJoinParam("{!join from=id " + method + "}*:*", "to");
+ }
+ }
+
+ private void assertMissingJoinParam(String query, String missing) {
+ SolrException e = expectThrows(SolrException.class, () -> h.query(req("q",
query, "fl", "id")));
+ assertEquals(query, SolrException.ErrorCode.BAD_REQUEST.code, e.code());
+ assertTrue(e.getMessage(), e.getMessage().contains("'" + missing + "'"));
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/search/join/CrossCollectionJoinQueryTest.java
b/solr/core/src/test/org/apache/solr/search/join/CrossCollectionJoinQueryTest.java
index 341be4d6223..2b6e50693fd 100644
---
a/solr/core/src/test/org/apache/solr/search/join/CrossCollectionJoinQueryTest.java
+++
b/solr/core/src/test/org/apache/solr/search/join/CrossCollectionJoinQueryTest.java
@@ -30,6 +30,7 @@ import
org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
@@ -469,6 +470,28 @@ public class CrossCollectionJoinQueryTest extends
SolrCloudTestCase {
}
}
+ @Test
+ public void testMissingFromOrToAtParser() throws Exception {
+ for (String missing : List.of(CrossCollectionJoinQParser.FROM,
CrossCollectionJoinQParser.TO)) {
+ ModifiableSolrParams localParams = new ModifiableSolrParams();
+ localParams.set(QueryParsing.V, "*:*");
+ localParams.set(CrossCollectionJoinQParser.FROM_INDEX, "products");
+ localParams.set(CrossCollectionJoinQParser.FROM, "product_id_s");
+ localParams.set(CrossCollectionJoinQParser.TO, "product_id_s");
+ localParams.remove(missing);
+
+ ModifiableSolrParams requestParams = new ModifiableSolrParams();
+ try (SolrQueryRequest req = new SolrQueryRequestBase(null,
requestParams) {}) {
+ CrossCollectionJoinQParser parser =
+ new CrossCollectionJoinQParser(
+ null, localParams, requestParams, req, "product_id_s", null);
+ SolrException e = expectThrows(SolrException.class, parser::parse);
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
+ assertTrue(e.getMessage(), e.getMessage().contains("'" + missing +
"'"));
+ }
+ }
+ }
+
@Test
public void testShardsPreferenceRequestParamPropagation() throws Exception {
// shards.preference set as a request-level param (not in localParams)
should be
diff --git
a/solr/core/src/test/org/apache/solr/search/join/TestAuxIndexJoinQParserPlugin.java
b/solr/core/src/test/org/apache/solr/search/join/TestAuxIndexJoinQParserPlugin.java
index 4c1871f8d36..9685ed74bac 100644
---
a/solr/core/src/test/org/apache/solr/search/join/TestAuxIndexJoinQParserPlugin.java
+++
b/solr/core/src/test/org/apache/solr/search/join/TestAuxIndexJoinQParserPlugin.java
@@ -119,12 +119,15 @@ public class TestAuxIndexJoinQParserPlugin extends
SolrTestCase {
@Test
public void testMissingLocalParams() {
+ assertMissingJoinParam("{!auxIndexJoin to=dept_id_s}*:*", "from");
+ assertMissingJoinParam("{!auxIndexJoin from=dept_s}*:*", "to");
+ }
+
+ private void assertMissingJoinParam(String query, String missing) {
SolrException e =
- expectThrows(
- SolrException.class,
- () -> solrRule.getSolrClient().query(params("q", "{!auxIndexJoin
to=dept_id_s}*:*")));
+ expectThrows(SolrException.class, () ->
solrRule.getSolrClient().query(params("q", query)));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
- assertTrue(e.getMessage(), e.getMessage().contains("requires"));
+ assertTrue(e.getMessage(), e.getMessage().contains("missing required '" +
missing + "'"));
}
@Test
diff --git
a/solr/core/src/test/org/apache/solr/search/join/TestGlobalOrdinalsJoinQParser.java
b/solr/core/src/test/org/apache/solr/search/join/TestGlobalOrdinalsJoinQParser.java
index 7cd72a52516..04eebca7f54 100644
---
a/solr/core/src/test/org/apache/solr/search/join/TestGlobalOrdinalsJoinQParser.java
+++
b/solr/core/src/test/org/apache/solr/search/join/TestGlobalOrdinalsJoinQParser.java
@@ -442,6 +442,19 @@ public class TestGlobalOrdinalsJoinQParser extends
SolrTestCaseJ4 {
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
assertTrue(ex.getMessage().contains("which"));
+ // Missing from query (no query body and no 'v' local param)
+ ex =
+ expectThrows(
+ SolrException.class,
+ () -> {
+ h.query(
+ req(
+ "q",
+ "{!globalOrdinalsJoin joinField=join_s_dv
which=\\\"type_s:parent\\\"}"));
+ });
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+ assertTrue(ex.getMessage().contains("from query is required"));
+
// Invalid score mode
ex =
expectThrows(