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