You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ep...@apache.org on 2026/10/08 22:46:54 UTC

(solr) branch main updated: SOLR-9759: Admin UI should post streaming expressions (#5048)

This is an automated email from the ASF dual-hosted git repository.

epugh 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 c6f910d3ccf SOLR-9759: Admin UI should post streaming expressions (#5048)
c6f910d3ccf is described below

commit c6f910d3ccf188a10fbe155ad6253b5236b4a6dc
Author: Eric Pugh <ep...@opensourceconnections.com>
AuthorDate: Thu Oct 8 18:46:48 2026 -0400

    SOLR-9759: Admin UI should post streaming expressions (#5048)
---
 .../unreleased/solr-9759-admin-ui-stream-post.yml  |  8 +++++
 .../solr/webapp/AdminUiStreamScreenTest.java       | 41 ++++++++++++++++++++++
 solr/webapp/web/js/angular/controllers/stream.js   | 37 +++++++++++--------
 solr/webapp/web/js/angular/services.js             | 39 +++++++++++++-------
 4 files changed, 99 insertions(+), 26 deletions(-)

diff --git a/changelog/unreleased/solr-9759-admin-ui-stream-post.yml b/changelog/unreleased/solr-9759-admin-ui-stream-post.yml
new file mode 100644
index 00000000000..67bf904139d
--- /dev/null
+++ b/changelog/unreleased/solr-9759-admin-ui-stream-post.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Admin UI's Stream screen now posts the streaming expression as a form-encoded body instead of a GET query-string parameter, so large expressions no longer silently fail, and a failed request now shows an error instead of leaving the screen blank.
+type: fixed
+authors:
+  - name: Eric Pugh
+links:
+  - name: SOLR-9759
+    url: https://issues.apache.org/jira/browse/SOLR-9759
diff --git a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java
index b348120bceb..3abec954fdc 100644
--- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java
+++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiStreamScreenTest.java
@@ -21,6 +21,7 @@ import org.apache.solr.common.SolrInputDocument;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
 import org.openqa.selenium.WebElement;
 
 /** Tests the Stream screen: executing a streaming expression through the form. */
@@ -51,4 +52,44 @@ public class AdminUiStreamScreenTest extends AdminUiTestBase {
     assertTrue("All docs should stream: " + response, response.contains("stream-doc-3"));
     assertNoSevereConsoleErrors();
   }
+
+  @Test
+  public void testLargeExpressionSucceedsViaUi() {
+    // A streaming expression large enough that a GET request's URL/header would be rejected by
+    // Jetty before ever reaching Solr (SOLR-9759) - a single wildcard clause keeps it one simple
+    // query (matching nothing, since no real id starts with this), so a clean zero-hit response
+    // (rather than a hang, a truncated request, or a parse error) confirms the whole POST body
+    // round-tripped intact.
+    String padding = "a".repeat(20000);
+    String expression =
+        "search(" + COLLECTION + ",q=\"*:*\",fl=\"id\",sort=\"id asc\",fq=\"id:" + padding + "*\")";
+    assertTrue(
+        "test expression should exceed a typical 8K header/URL limit", expression.length() > 16384);
+
+    openPage(COLLECTION + "/stream", By.id("stream"));
+    WebElement expr = waitFor(By.id("expr"));
+    ((JavascriptExecutor) driver)
+        .executeScript(
+            "arguments[0].value = arguments[1];"
+                + "arguments[0].dispatchEvent(new Event('input', {bubbles: true}));",
+            expr,
+            expression);
+    click(By.cssSelector("#stream button[type=submit]"));
+    waitForTextContains(By.cssSelector("#stream #result"), "EOF");
+    assertNoSevereConsoleErrors();
+  }
+
+  @Test
+  public void testFailedRequestShowsErrorInsteadOfHanging() {
+    // a nonexistent collection makes the request fail - confirms a failed request surfaces
+    // something in the UI instead of leaving the screen blank forever (the original bug).
+    openPage("nonexistentcoll/stream", By.id("stream"));
+    WebElement expr = waitFor(By.id("expr"));
+    expr.clear();
+    expr.sendKeys("search(" + COLLECTION + ",q=\"*:*\",fl=\"id\",sort=\"id asc\")");
+    click(By.cssSelector("#stream button[type=submit]"));
+    waitForTextContains(By.cssSelector("#stream #result"), "no handler, collection, or core");
+    // the request is expected to fail (404) - that's the scenario under test
+    assertNoSevereConsoleErrors("404 (Not Found)");
+  }
 }
diff --git a/solr/webapp/web/js/angular/controllers/stream.js b/solr/webapp/web/js/angular/controllers/stream.js
index f8001d84ffe..56faeb4d6d5 100644
--- a/solr/webapp/web/js/angular/controllers/stream.js
+++ b/solr/webapp/web/js/angular/controllers/stream.js
@@ -41,27 +41,36 @@ solrAdminApp.controller('StreamController',
       $scope.response = null;
       $scope.url = "";
 
+      // Shown for reference only - the request is actually sent as a POST body below, since a
+      // streaming expression can be too large for a URL/header (SOLR-9759).
       var url = Query.url(params);
 
-      Query.query(params, function(data) {
-
-        var jsonData = JSON.parse(data.toJSON().data);
-        if (undefined != jsonData["explanation"]) {
-          $scope.showExplanation = true;
-
-          streamGraphSubController($scope, jsonData["explanation"])
-          delete jsonData["explanation"]
-        } else {
-          $scope.showExplanation = false;
+      // Handles both the success and error callbacks below: app.js's global interceptor
+      // (see failed()'s doNotIntercept branch) routes most failures for this request through
+      // the "success" callback too, so this must defend against a non-JSON body either way.
+      var showResult = function(raw) {
+        $scope.showExplanation = false;
+        try {
+          var jsonData = JSON.parse(raw);
+          if (undefined != jsonData["explanation"]) {
+            $scope.showExplanation = true;
+            streamGraphSubController($scope, jsonData["explanation"]);
+            delete jsonData["explanation"];
+          }
+          raw = JSON.stringify(jsonData, null, 2);
+        } catch (e) {
+          // not JSON (e.g. a raw HTTP error page) - show it as-is rather than crashing
         }
-
-        data.data = JSON.stringify(jsonData,null,2);
-
         $scope.lang = "json";
-        $scope.response = data;
+        $scope.response = {data: raw};
         $scope.url = url;
         $scope.hostPortContext = $location.absUrl().substr(0,$location.absUrl().indexOf("#")); // For display only
+      };
 
+      Query.queryPost({core: params.core, handler: params.handler}, params, function(data) {
+        showResult(data.toJSON().data);
+      }, function(rejection) {
+        showResult((rejection.data && rejection.data.data) || ("HTTP " + rejection.status + " " + rejection.statusText));
       });
     };
 
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index 462d684121e..a088e1c1c6f 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -355,25 +355,40 @@ solrAdminServices.factory('Metrics',
   }])
 .factory('Query',
     ['$resource', function($resource) {
-       var resource = $resource(':core/:handler', {core: '@core', handler: '@handler', '_':Date.now()}, {
-           "query": {
-             method: "GET",
-             transformResponse: function (data) {
-               return {data: data}
-             },
-             headers: {doNotIntercept: "true"}
-           }
-       });
-       resource.url = function(params) {
+       var toQueryString = function(params) {
            var qs = [];
-           for (key in params) {
+           for (var key in params) {
                if (key != "core" && key != "handler") {
                    for (var i in params[key]) {
                        qs.push(key + "=" + encodeURIComponent(params[key][i]));
                    }
                }
            }
-           return "" + params.core + "/" + params.handler + "?" + qs.sort().join("&");
+           return qs.sort().join("&");
+       }
+       var wrapRawResponse = function(data) {
+           return {data: data}
+       }
+       var resource = $resource(':core/:handler', {core: '@core', handler: '@handler', '_':Date.now()}, {
+           "query": {
+             method: "GET",
+             transformResponse: wrapRawResponse,
+             headers: {doNotIntercept: "true"}
+           },
+           // Same request as "query" above, but as a form-encoded POST body instead of a query
+           // string - for request params (e.g. a streaming expression) too large for a URL/header.
+           "queryPost": {
+             method: "POST",
+             transformRequest: toQueryString,
+             transformResponse: wrapRawResponse,
+             headers: {
+               'Content-Type': 'application/x-www-form-urlencoded',
+               doNotIntercept: "true"
+             }
+           }
+       });
+       resource.url = function(params) {
+           return "" + params.core + "/" + params.handler + "?" + toQueryString(params);
        }
        return resource;
 }])