This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_9x in repository https://gitbox.apache.org/repos/asf/solr.git
commit df068c71f8b25a2e0ded86d722b0eeb1c84c5f5b 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) Adapted for branch_9x: omits the auxIndexJoin and globalOrdinalsJoin parts, which are not on 9.x. (cherry picked from commit 93e357bcf6bc9b3b6195043529ef5e1fa2001fde) (cherry picked from commit a9d489d2035d6ee920f2068f025c6217a934603a) --- changelog/unreleased/SOLR-13202.yml | 7 +++++++ .../org/apache/solr/search/JoinQParserPlugin.java | 2 ++ .../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 ++++++++++++++++++++++ 6 files changed, 61 insertions(+) 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 6bf843f8c30..43ea5612141 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/CrossCollectionJoinQParser.java b/solr/core/src/java/org/apache/solr/search/join/CrossCollectionJoinQParser.java index 40c69412741..7c00243acf2 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 @@ -98,6 +98,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 cc227b086b4..0a62b5db219 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 @@ -238,6 +238,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) { @@ -247,6 +259,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 c257f077918..a360698b8fe 100644 --- a/solr/core/src/test/org/apache/solr/TestJoin.java +++ b/solr/core/src/test/org/apache/solr/TestJoin.java @@ -486,4 +486,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 d1d69409172..f44883be9dc 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; @@ -445,6 +446,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
