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 cdc1707abb4 SOLR-16640: Admin UI's SQL screen shows an error instead
of crashing (#5057)
cdc1707abb4 is described below
commit cdc1707abb4d1071b880236fe418cb17d7729e50
Author: Eric Pugh <[email protected]>
AuthorDate: Sat Oct 10 07:49:58 2026 -0400
SOLR-16640: Admin UI's SQL screen shows an error instead of crashing (#5057)
---
.../solr-16640-admin-ui-sql-error-handling.yml | 8 ++
.../java/org/apache/solr/cli/RunExampleTool.java | 2 +-
.../apache/solr/webapp/AdminUiSqlScreenTest.java | 52 +++++++++++--
solr/webapp/web/js/angular/controllers/sqlquery.js | 85 ++++++++++++++--------
solr/webapp/web/partials/sqlquery.html | 5 +-
5 files changed, 114 insertions(+), 38 deletions(-)
diff --git a/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml
b/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml
new file mode 100644
index 00000000000..9a628d567e5
--- /dev/null
+++ b/changelog/unreleased/solr-16640-admin-ui-sql-error-handling.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Admin UI's SQL screen no longer crashes with an uncaught JS error (and
silently shows a blank result grid) when the response isn't a SQL result-set -
e.g. when the sql module/handler isn't installed. It now shows the server's
error message instead.
+type: fixed
+authors:
+ - name: Eric Pugh
+links:
+ - name: SOLR-16640
+ url: https://issues.apache.org/jira/browse/SOLR-16640
diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
index 9dddc9938d3..66828271c06 100644
--- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java
@@ -1026,7 +1026,7 @@ public class RunExampleTool extends ToolBase {
final var syspropArg =
("techproducts".equals(params.example()))
- ? "-Dsolr.modules=clustering,extraction,langid,ltr,scripting
-Dsolr.ltr.enabled=true -Dsolr.clustering.enabled=true"
+ ? "-Dsolr.modules=clustering,extraction,langid,ltr,scripting,sql
-Dsolr.ltr.enabled=true -Dsolr.clustering.enabled=true"
: "";
String startCmdStr =
diff --git
a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java
b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java
index c49871834ce..837ddb5439e 100644
--- a/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java
+++ b/solr/webapp/src/test/org/apache/solr/webapp/AdminUiSqlScreenTest.java
@@ -16,17 +16,16 @@
*/
package org.apache.solr.webapp;
+import java.util.Locale;
import org.apache.solr.client.solrj.SolrClient;
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 SQL screen: executing a SQL query through the form. Requires the
sql module on the
- * server classpath, provided by the webapp test dependencies.
- */
+/** Browser tests for the SQL screen. The test classpath includes the sql
module. */
public class AdminUiSqlScreenTest extends AdminUiTestBase {
private static final String COLLECTION = "sqlcoll";
@@ -51,10 +50,53 @@ public class AdminUiSqlScreenTest extends AdminUiTestBase {
stmt.sendKeys("SELECT id FROM " + COLLECTION + " LIMIT 10");
click(By.xpath("//div[@id='sqlquery']//button[@type='submit']"));
- // the result grid lists all documents
for (int i = 1; i <= 3; i++) {
waitForPageContains("sql-doc-" + i);
}
assertNoSevereConsoleErrors();
}
+
+ @Test
+ public void testFailedRequestShowsErrorInsteadOfCrashing() {
+ // A missing collection produces an error response without a result-set.
+ openPage("nonexistentcoll/sqlquery", By.id("sqlquery"));
+ WebElement stmt = waitFor(By.id("sqlexp"));
+ stmt.clear();
+ stmt.sendKeys("SELECT id FROM " + COLLECTION + " LIMIT 10");
+ click(By.xpath("//div[@id='sqlquery']//button[@type='submit']"));
+ waitForTextContains(By.id("sql-response"), "no handler, collection, or
core");
+ assertNoSevereConsoleErrors("404 (Not Found)");
+ }
+
+ @Test
+ public void testSqlModuleNotEnabledShowsFriendlyMessage() {
+ // Use a captured missing-module response because the test classpath
includes the sql module.
+ openPage(COLLECTION + "/sqlquery", By.id("sqlquery"));
+ waitFor(By.id("sqlexp"));
+ String classNotFoundResponse =
+
"{\"error\":{\"metadata\":{\"error-class\":\"org.apache.solr.common.SolrException\","
+ + "\"root-error-class\":\"java.lang.ClassNotFoundException\"},"
+ + "\"errorClass\":\"org.apache.solr.common.SolrException\","
+ + "\"msg\":\" Error loading class
'solr.SQLHandler'\",\"code\":500}}";
+ String sqlError =
+ (String)
+ ((JavascriptExecutor) driver)
+ .executeScript(
+ "var scope =
angular.element(document.getElementById('sqlquery')).scope();"
+ + "scope.showResult(arguments[0]);"
+ + "scope.$apply();"
+ + "return scope.sqlError;",
+ classNotFoundResponse);
+ assertTrue(
+ "should show a friendly message, got: " + sqlError,
+ sqlError != null && sqlError.toLowerCase(Locale.ROOT).contains("sql
module"));
+ assertTrue(
+ "should say how to fix it: " + sqlError,
+ sqlError.toLowerCase(Locale.ROOT).contains("enable"));
+ WebElement documentation = waitFor(By.cssSelector("#sql-response span a"));
+ assertEquals(
+ "https://solr.apache.org/guide/solr/latest/query-guide/sql-query.html",
+ documentation.getDomAttribute("href"));
+ assertEquals("_out", documentation.getDomAttribute("target"));
+ }
}
diff --git a/solr/webapp/web/js/angular/controllers/sqlquery.js
b/solr/webapp/web/js/angular/controllers/sqlquery.js
index 7aabc887d5e..b8057dd8e60 100644
--- a/solr/webapp/web/js/angular/controllers/sqlquery.js
+++ b/solr/webapp/web/js/angular/controllers/sqlquery.js
@@ -31,6 +31,55 @@ solrAdminApp.controller('SQLQueryController',
}
};
$scope.hostPortContext =
$location.absUrl().substr(0,$location.absUrl().indexOf("#")); // For display
only
+
+ // The global interceptor can route errors to the success callback, so
handle both here.
+ $scope.showResult = function(raw) {
+ $scope.lang = "json";
+ $scope.sqlError = null;
+ $scope.sqlModuleMissing = false;
+ $scope.sqlData = [];
+
+ var jsonData;
+ try {
+ jsonData = JSON.parse(raw);
+ } catch (e) {
+ $scope.sqlError = raw;
+ return;
+ }
+
+ var docs = jsonData && jsonData['result-set'] &&
jsonData['result-set'].docs;
+ if (!docs) {
+ var err = jsonData && jsonData.error;
+ if (err && err.metadata && err.metadata['root-error-class'] ===
'java.lang.ClassNotFoundException'
+ && err.msg && err.msg.indexOf('SQLHandler') !== -1) {
+ $scope.sqlModuleMissing = true;
+ $scope.sqlError = "The sql module doesn't appear to be enabled on
this Solr node.";
+ } else {
+ $scope.sqlError = (jsonData && jsonData.message) || (err && err.msg)
|| raw;
+ }
+ return;
+ }
+
+ for (var i = 0; i < docs.length; i++) {
+ var doc = docs[i]
+ if(doc.hasOwnProperty("EOF")){
+ if(doc.hasOwnProperty("EXCEPTION")){
+ $scope.sqlError = doc.EXCEPTION
+ }
+ } else {
+ $scope.gridOptions.data.push(doc);
+ }
+ }
+ // Build grid columns from the result fields.
+ var fields = $scope.gridOptions.data[1];
+ for (var property in fields) {
+ if (fields.hasOwnProperty(property)) {
+ $scope.gridOptions.columnDefs.push({"name":property, "type":{}})
+ }
+ }
+ $scope.gridApi.core.notifyDataChange
+ };
+
$scope.doQuery = function() {
var params = {};
@@ -47,41 +96,15 @@ solrAdminApp.controller('SQLQueryController',
$scope.lang = "json";
$scope.response = null;
- $scope.url = "";
$scope.gridOptions.data =[]
$scope.gridOptions.columnDefs = []
- var url = Query.url(params);
- Query.query(params, function(data) {
+ $scope.url = Query.url(params);
- var jsonData = JSON.parse(data.toJSON().data);
- $scope.lang = "json";
- $scope.url = url;
- $scope.sqlError = null;
- $scope.sqlData = [];
- if(jsonData != undefined){
- var docs = jsonData['result-set'].docs
- //get all docs
- for (var i = 0; i < docs.length; i++) {
- var doc = docs[i]
- //get all the properties
- if(doc.hasOwnProperty("EOF")){
- if(doc.hasOwnProperty("EXCEPTION")){
- $scope.sqlError = doc.EXCEPTION
- }
- } else {
- $scope.gridOptions.data.push(doc);
- }
- }
- }
- //Build the columnFields from data
- var fields = $scope.gridOptions.data[1];
- for (var property in fields) {
- if (fields.hasOwnProperty(property)) {
- $scope.gridOptions.columnDefs.push({"name":property,
"type":{}})
- }
- }
- $scope.gridApi.core.notifyDataChange
+ Query.query(params, function(data) {
+ $scope.showResult(data.toJSON().data);
+ }, function(rejection) {
+ $scope.showResult((rejection.data && rejection.data.data) || ("HTTP "
+ rejection.status + " " + rejection.statusText));
});
};
}
diff --git a/solr/webapp/web/partials/sqlquery.html
b/solr/webapp/web/partials/sqlquery.html
index c0254d13ac3..76a2506c061 100644
--- a/solr/webapp/web/partials/sqlquery.html
+++ b/solr/webapp/web/partials/sqlquery.html
@@ -28,7 +28,10 @@ limitations under the License.
</div>
<div id="sql-response">
<a ng-show="sqlData" id="url" class="address-bar"
ng-href="{{url}}">{{hostPortContext}}{{url}}</a>
- <div ng-show="sqlError"> {{sqlError}}</div>
+ <div ng-show="sqlError">
+ {{sqlError}}
+ <span ng-if="sqlModuleMissing">See <a
href="https://solr.apache.org/guide/solr/latest/query-guide/sql-query.html"
target="_out">SQL documentation</a> for how to enable it.</span>
+ </div>
<div ng-show="!sqlError" class="grid" ui-grid="gridOptions"
id="sql-response"></div>
</div>