Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/solr-9759-admin-ui-stream-post.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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. */
Expand Down Expand Up @@ -51,4 +52,44 @@ public void testStreamingExpressionViaUi() {
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)");
}
}
37 changes: 23 additions & 14 deletions solr/webapp/web/js/angular/controllers/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
};

Expand Down
39 changes: 27 additions & 12 deletions solr/webapp/web/js/angular/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}])
Expand Down
Loading