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 <[email protected]>
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;
 }])

Reply via email to