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 000000000000..9a628d567e51 --- /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 e109022e59ab..6776ce9f7c31 100644 --- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java +++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java @@ -814,7 +814,7 @@ Map startSolr( 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 c49871834ceb..837ddb5439e0 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 void testSqlQueryViaUi() { 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 7aabc887d5e4..b8057dd8e60e 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 c0254d13ac34..76a2506c061b 100644 --- a/solr/webapp/web/partials/sqlquery.html +++ b/solr/webapp/web/partials/sqlquery.html @@ -28,7 +28,10 @@
{{hostPortContext}}{{url}} -
{{sqlError}}
+
+ {{sqlError}} + See SQL documentation for how to enable it. +