Add structured logging and better error handling
This commit is contained in:
1 parent
db4e3c9611
commit
68c7dd76f2
31 files changed
+640
-109
No files matched your search
+42
-5
@@ -1,8 +1,22 @@
|
||||
export class ApiError extends Error {
|
||||
constructor(status, message) {
|
||||
constructor(status, message, requestId = "") {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
get isNetworkError() {
|
||||
return this.status === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function describeError(error) {
|
||||
const message = error && error.message ? error.message : "Something went wrong.";
|
||||
const alreadyReferenced = message.includes("reference");
|
||||
if (error instanceof ApiError && error.requestId && error.status >= 500 && !alreadyReferenced) {
|
||||
return `${message} (reference ${error.requestId})`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
const unauthorizedListeners = new Set();
|
||||
@@ -20,6 +34,22 @@ function parseBody(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackMessage(status) {
|
||||
if (status === 502 || status === 503 || status === 504) {
|
||||
return `The server is not responding right now (${status}). Try again in a moment.`;
|
||||
}
|
||||
if (status === 413) return "The request was too large.";
|
||||
return `Request failed (${status}).`;
|
||||
}
|
||||
|
||||
async function send(path, options) {
|
||||
try {
|
||||
return await fetch(path, options);
|
||||
} catch {
|
||||
throw new ApiError(0, "Cannot reach the server. Check your connection.");
|
||||
}
|
||||
}
|
||||
|
||||
async function request(method, path, body) {
|
||||
const options = { method, credentials: "same-origin", headers: {} };
|
||||
if (body !== undefined) {
|
||||
@@ -27,15 +57,22 @@ async function request(method, path, body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(path, options);
|
||||
const data = parseBody(await response.text());
|
||||
const response = await send(path, options);
|
||||
let text = "";
|
||||
try {
|
||||
text = await response.text();
|
||||
} catch {
|
||||
throw new ApiError(0, "The connection dropped while reading the server response.");
|
||||
}
|
||||
const data = parseBody(text);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && path !== "/api/login") {
|
||||
unauthorizedListeners.forEach((listener) => listener());
|
||||
}
|
||||
const message = data && data.error ? data.error : `Request failed (${response.status})`;
|
||||
throw new ApiError(response.status, message);
|
||||
const message = data && data.error ? data.error : fallbackMessage(response.status);
|
||||
const requestId = (data && data.requestId) || response.headers.get("X-Request-ID") || "";
|
||||
throw new ApiError(response.status, message, requestId);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
+31
-10
@@ -1,4 +1,4 @@
|
||||
import { api, onUnauthorized } from "./api.js";
|
||||
import { api, describeError, onUnauthorized } from "./api.js";
|
||||
import { element } from "./dom.js";
|
||||
import { destinationSelect, setAutoExtractDefault, setupDownloadForm, updateSubfolderState } from "./download-form.js";
|
||||
import { formatBytes } from "./format.js";
|
||||
@@ -12,6 +12,7 @@ import { renderTree, topLevelSelection } from "./tree.js";
|
||||
|
||||
const pollIntervalMs = 1500;
|
||||
const filterStorageKey = "ddc-job-filter";
|
||||
const failuresBeforeBanner = 2;
|
||||
|
||||
const elements = {
|
||||
loginView: document.getElementById("login-view"),
|
||||
@@ -21,6 +22,7 @@ const elements = {
|
||||
logoutButton: document.getElementById("logout-button"),
|
||||
settingsButton: document.getElementById("settings-button"),
|
||||
diskStatus: document.getElementById("disk-status"),
|
||||
connectionBanner: document.getElementById("connection-banner"),
|
||||
jobFilter: document.getElementById("job-filter"),
|
||||
clearCompletedButton: document.getElementById("clear-completed-button"),
|
||||
jobList: document.getElementById("job-list"),
|
||||
@@ -53,10 +55,12 @@ const state = {
|
||||
selection: new Set(),
|
||||
pollTimer: null,
|
||||
refreshing: false,
|
||||
failedRefreshes: 0,
|
||||
};
|
||||
|
||||
function showLogin() {
|
||||
stopPolling();
|
||||
setConnectionProblem(null);
|
||||
closeDetail();
|
||||
elements.appView.hidden = true;
|
||||
elements.loginView.hidden = false;
|
||||
@@ -82,7 +86,7 @@ async function showSettings() {
|
||||
try {
|
||||
await openSettings();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
window.alert(describeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,15 +131,31 @@ async function refresh() {
|
||||
updateSpeeds(state.jobs);
|
||||
handleTransitions(collectTransitions(state.jobs));
|
||||
elements.diskStatus.textContent = `${formatBytes(disk.availableBytes)} free`;
|
||||
setConnectionProblem(null);
|
||||
renderJobList();
|
||||
await syncDetail();
|
||||
} catch (error) {
|
||||
if (error.status !== 401) console.error(error);
|
||||
if (error.status !== 401) {
|
||||
console.error(error);
|
||||
setConnectionProblem(error);
|
||||
}
|
||||
} finally {
|
||||
state.refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setConnectionProblem(error) {
|
||||
if (!error) {
|
||||
state.failedRefreshes = 0;
|
||||
elements.connectionBanner.hidden = true;
|
||||
return;
|
||||
}
|
||||
state.failedRefreshes += 1;
|
||||
if (state.failedRefreshes < failuresBeforeBanner) return;
|
||||
elements.connectionBanner.textContent = `${describeError(error)} Retrying automatically.`;
|
||||
elements.connectionBanner.hidden = false;
|
||||
}
|
||||
|
||||
function handleTransitions(transitions) {
|
||||
notifyTransitions(transitions);
|
||||
if (transitions.some((transition) => transition.previous === "moving")) loadHistory();
|
||||
@@ -166,7 +186,7 @@ async function runJobAction(action) {
|
||||
await action();
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
window.alert(describeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +203,7 @@ function openDetail(job) {
|
||||
renderJobList();
|
||||
syncDetail();
|
||||
loadLibraries().catch((error) => {
|
||||
elements.actionError.textContent = error.message;
|
||||
elements.actionError.textContent = describeError(error);
|
||||
});
|
||||
elements.detail.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
@@ -230,7 +250,7 @@ async function loadTree(jobId) {
|
||||
});
|
||||
updateSelectionSummary();
|
||||
} catch (error) {
|
||||
elements.actionError.textContent = error.message;
|
||||
elements.actionError.textContent = describeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +267,7 @@ async function extractArchive(path) {
|
||||
await api.extract(state.selectedJobId, path);
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
elements.actionError.textContent = error.message;
|
||||
elements.actionError.textContent = describeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +286,7 @@ async function moveSelection() {
|
||||
elements.subfolderInput.value = "";
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
elements.actionError.textContent = error.message;
|
||||
elements.actionError.textContent = describeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +301,7 @@ async function deleteJob(job) {
|
||||
if (job.id === state.selectedJobId) closeDetail();
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
window.alert(describeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +324,7 @@ async function clearHistory() {
|
||||
await api.clearHistory();
|
||||
await loadHistory();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
window.alert(describeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +356,7 @@ async function logout() {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
window.addEventListener("unhandledrejection", (event) => console.error("Unhandled error", event.reason));
|
||||
onUnauthorized(showLogin);
|
||||
setupLogin(elements.loginForm, elements.loginError, showApp);
|
||||
setupDownloadForm(refresh);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from "./api.js";
|
||||
import { api, describeError } from "./api.js";
|
||||
|
||||
const fields = {
|
||||
form: document.getElementById("download-form"),
|
||||
@@ -61,7 +61,7 @@ async function submitDownloads(event, onCreated) {
|
||||
});
|
||||
} catch (error) {
|
||||
failedUrls.push(url);
|
||||
if (!firstError) firstError = error.message;
|
||||
if (!firstError) firstError = describeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from "./api.js";
|
||||
import { api, describeError } from "./api.js";
|
||||
import { element } from "./dom.js";
|
||||
import { notificationsEnabled, setNotificationsEnabled } from "./notify.js";
|
||||
|
||||
@@ -77,7 +77,7 @@ async function saveSettings(event, onSaved) {
|
||||
fields.dialog.close();
|
||||
onSaved(data.settings);
|
||||
} catch (error) {
|
||||
fields.error.textContent = error.message;
|
||||
fields.error.textContent = describeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user