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

Reply via email to