Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consistently use fetch + improve error handling #69

Merged
merged 27 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
53b8358
Replace $.ajax with calls to the fetch API and clean up codebase
RobinTF Nov 25, 2023
570a142
Replace usages of %.post with fetch
RobinTF Nov 25, 2023
4e593be
Replace $.getJSON with fetch
RobinTF Nov 25, 2023
1508b6b
Add back missing variable
RobinTF Nov 25, 2023
6c31d20
Add let to variable declaration
RobinTF Nov 27, 2023
ae1d112
Remove redundant content-type declaration
RobinTF Nov 27, 2023
ee5a331
Add documentation and convert let to const
RobinTF Dec 2, 2023
b6cbd5d
Fix typo
RobinTF Dec 2, 2023
e691d2d
Fix bad error handling
RobinTF Dec 2, 2023
a398771
Remove redundant semicolon
RobinTF Dec 2, 2023
6ba8c1e
Use nicer error message if JSON cannot be parsed
RobinTF Dec 3, 2023
1e5b229
Fine tune error message
RobinTF Dec 3, 2023
bb4ce7d
Fix double encoding
RobinTF Dec 9, 2023
66ab63b
Make use of POST body
RobinTF Dec 9, 2023
fd3af60
Remove redundant code
RobinTF Dec 9, 2023
26c87e1
Only pass error message to log
RobinTF Dec 9, 2023
823c3b8
Largely unify error handling
RobinTF Dec 9, 2023
1603983
Fix duplicate handler registration
RobinTF Dec 9, 2023
5d9c6ed
Merge remote-tracking branch 'ad-freiburg/master' into migrate-to-fetch
RobinTF Dec 10, 2023
249682a
Small formatting changes
RobinTF Dec 10, 2023
84b4589
Change error message for HTTP 502, 503, or 504
Dec 11, 2023
e87cb64
Run catch block if stats request is unsuccessful
RobinTF Dec 11, 2023
44a9feb
Fix problem with cmd=stats + improve error message
Dec 11, 2023
62450d8
Properly propagate AbortErrors
RobinTF Dec 11, 2023
117ad82
Fix invalid JSON error handling
RobinTF Dec 11, 2023
f240d52
Add back accept header
RobinTF Dec 13, 2023
f285d8d
Use qlever-flavour JSON
RobinTF Dec 13, 2023
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
26 changes: 7 additions & 19 deletions backend/static/js/codemirror/modes/sparql/sparql-hint.js
Original file line number Diff line number Diff line change
Expand Up @@ -724,21 +724,14 @@ function parseAndEvaluateCondition(condition, word, lines, words) {
// then only work for SPARQL enpoints supporting a "timeout" argument. Note that
// Virtuoso does have such an argument, too, but the unit is milliseconds and not
// seconds like for QLever.
const fetchTimeout = (sparqlQuery, timeoutSeconds, { ...options } = {}) => {
const fetchTimeout = (sparqlQuery, timeoutSeconds) => {
const timeoutMilliseconds = timeoutSeconds * 1000;
const controller = new AbortController();
// `BASEURL` is defined in backend/templates/partials/head.html . It's the
// base URL from the backend configuration.
const promise = fetch(BASEURL, {
method: "POST",
body: sparqlQuery,
signal: controller.signal,
headers: {
"Content-type": "application/sparql-query",
"Accept": "application/qlever-results+json"
},
...options
});
const promise = fetchQleverBackend(
{ query: sparqlQuery },
{},
{ signal: controller.signal }
);
if (timeoutMilliseconds > 0) {
const timeout = setTimeout(() => controller.abort(), timeoutMilliseconds);
return promise.finally(() => clearTimeout(timeout));
Expand Down Expand Up @@ -839,12 +832,7 @@ function getQleverSuggestions(
}

// Get the actual query result as `data`.
let data;
if (allQueriesHaveTimedOut) {
data = {exception: "The request was cancelled due to timeout"};
} else {
data = await response.json();
}
const data = allQueriesHaveTimedOut ? { exception: "The request was cancelled due to timeout" } : response;

// Show the suggestions to the user.
//
Expand Down
146 changes: 51 additions & 95 deletions backend/static/js/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,54 @@ function normalizeQuery(query, escapeQuotes = false) {
.trim();
}


// Wrapper for `fetch` that turns potential errors into
// errors with user-friendly error messages.
async function fetchQleverBackend(params, additionalHeaders = {}, fetchOptions = {}) {
let response;
try {
response = await fetch(BASEURL, {
method: "POST",
body: new URLSearchParams(params),
headers: {
Accept: "application/qlever-results+json",
...additionalHeaders
},
...fetchOptions
});
} catch (error) {
// Rethrow abort errors directly
if (error.name === "AbortError") {
throw error;
}
throw new Error(`Cannot reach ${BASEURL}. The most common cause is that the QLever server is down. Please try again later and contact us if the error persists`);
}
switch(response.status) {
case 502:
throw new Error("502 Bad Gateway. The most common cause is a problem with the web server. Please try again later and contact us if the error perists");
case 503:
throw new Error("503 Service Unavailable. The most common cause is that the QLever server is down. Please try again later and contact us if the error perists");
case 504:
throw new Error("504 Gatway Timeout. The most common cause is that the query timed out. Please try again later and contact us if the error perists");
}
let text;
try {
text = await response.text();
} catch {
throw new Error('Server response was not valid UTF-8');
}
try {
return JSON.parse(text);
} catch {
// If response is not valid JSON and status is not 2xx,
// treat the text as error message.
if (!response.ok) {
throw new Error(text);
}
throw new Error(`Expected a JSON response, but got '${text}'`);
}
}

// Append the given runtime information for the given query to the runtime log.
//
// NOTE: A click on "Analysis" will show the runtime information from the last
Expand Down Expand Up @@ -277,13 +325,8 @@ async function enhanceQueryByNameTriples(query) {
test_query_parts["group_by"] = "";
test_query_parts["footer"] = "LIMIT 1";
test_query = createSparqlQueryFromParts(test_query_parts);
// console.log("TEST QUERY:", test_query);
const result = await fetch(BASEURL, {
method: "POST",
body: test_query,
headers: { "Content-type": "application/sparql-query" }
}).then(response => response.json());
if ("results" in result && result.results.bindings.length == 1) {
const result = await fetchQleverBackend({ query: test_query });
if (result?.res?.length === 1) {
// HACK: For variable ?pred use name_template_alt (see above).
new_vars[select_var] = select_var + new_var_suffix;
new_triples[select_var] = select_var != "?pred"
Expand Down Expand Up @@ -335,65 +378,6 @@ async function enhanceQueryByNameTriples(query) {
return new_query;
}

// Rewrite all SERVICE clauses in the query. For each such clause, launch the
// respective query against the respective endpoint, and turn the result into a
// VALUES clause, which then replaces the SERVICE clause.
//
// NOTE: Currently assumes that the SERVICE clause does not contain any "{ ... }.
async function rewriteServiceClauses(query) {

query = escapeCurlyBracesOfUnionMinusOptional(query);

var service_query_parts;
var service_clause_re = /SERVICE\s*<([^>]+)>\s*\{\s*([^}]+?)\s*\}/;
while (true) {
var service_clause_match = query.match(service_clause_re);
if (!service_clause_match) break;
const service_url = service_clause_match[1];
const service_where_clause = service_clause_match[2];
// Build the skeleton of the SERVICE query (needed at most once).
if (!service_query_parts) {
service_query_parts = splitSparqlQueryIntoParts(query);
service_query_parts["select_clause"] = "*";
service_query_parts["group_by"] = "";
service_query_parts["footer"] = "";
}
service_query_parts["body"] =
unescapeCurlyBracesOfUnionMinusOptional(service_where_clause);
const service_query = createSparqlQueryFromParts(service_query_parts);
console.log("SERVICE match:", service_clause_match);
console.log("Endpoint:", service_url);
console.log("Query:", service_where_clause);
const result_json = await fetch(service_url, {
method: "POST",
body: service_query,
headers: {
"Content-type": "application/sparql-query",
"Accept": "application/qlever-results+json"
// "Accept": "text/tab-separated-values"
}
}).then(response => response.json());
console.log("RESULT:", result_json);
var values_clause = "";
if (result_json.selected.length >= 1) {
values_clause =
"VALUES (" + result_json.selected.join(" ") + ")"
+ " { " +
result_json.res.map(tuple => "(" + tuple.join(" ") + ")").join(" ")
+ " }";
console.log("VALUES clause:", values_clause);
} else {
console.log("ERROR in processing SERVICE clause: no variables found, SERVICE clause will be ignored");
}
query = query.replace(service_clause_re, values_clause);
}

query = unescapeCurlyBracesOfUnionMinusOptional(query);

return query;
}


// Rewrite query in various ways, where the first three are synchronous (in a
// seperate function right below) and the fourth is asynchronous because it
// launches queries itself. Note that the first three are all HACKs, which
Expand Down Expand Up @@ -424,17 +408,6 @@ async function rewriteQuery(query, kwargs = {}) {
}
}

// Resolve SERVICE clauses in the query.
const rewrite_service_clauses = false;
if (rewrite_service_clauses) {
try {
query_rewritten = await rewriteServiceClauses(query_rewritten);
} catch(e) {
console.log("ERROR in \"rewriteServiceClauses\": " + e);
return query_rewritten;
}
}

return query_rewritten;
}

Expand Down Expand Up @@ -753,18 +726,13 @@ function expandEditor() {
}
}

function displayError(response, statusWithText = undefined, queryId = undefined) {
function displayError(response, queryId = undefined) {
console.error("Either the GET request failed or the backend returned an error:", response);
if (response["exception"] == undefined || response["exception"] == "") {
response["exception"] = "Unknown error";
}
disp = "<h4><strong>Error processing query</strong></h4>";
// if (statusWithText) { disp += "<p>" + statusWithText + "</p>"; }
disp += "<p>" + htmlEscape(response["exception"]) + "</p>";
// if (result["Exception-Error-Message"] == undefined || result["Exception-Error-Message"] == "") {
// result["Exception-Error-Message"] = "Unknown error";
// }
// disp = "<h3>Error:</h3><h4><strong>" + result["Exception-Error-Message"] + "</strong></h4>";
// The query sometimes is a one-element array with the query TODO: find out why.
if (Array.isArray(response.query)) {
if (response.query.length >= 1) { response.query = response.query[0]; }
Expand Down Expand Up @@ -819,18 +787,6 @@ function displayStatus(str) {
$("#infoBlock").show();
}

// This is used only in commented out code in the function `processQuery` in
// `qleverUI.js`.
function showAllConcatsDeprecated(element, sep, column) {
data = $(element).parent().data('original-title');
html = "";
results = data.split(sep);
for (var k = 0; k < results.length; k++) {
html += getFormattedResultEntry(results[k], 50, column)[0] + "<br>";
}
$(element).parent().html(html);
}

function tsep(str) {
var spl = str.toString().split('.');
var intP = spl[0];
Expand Down
Loading