From 9fc336b8946b83e2869fdf7381dabe68440a78a8 Mon Sep 17 00:00:00 2001 From: Serhii Orlivskyi Date: Fri, 9 Jan 2026 20:34:03 +0100 Subject: [PATCH] Dashboard: Add abort controller to abort in-flight request on navigation Known bug: abort controller logic doesn't work properly in firefox because a general network error is fired before the abort callback. We would probably need to track the navigation-click event to overcome this. Signed-off-by: Serhii Orlivskyi (Cedalo GmbH) --- dashboard/src/app/dashboard.js | 20 +++++++++++++++----- dashboard/src/app/listeners.js | 19 ++++++++++++++++--- dashboard/src/utils/utils.js | 15 +++++++++++++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/dashboard/src/app/dashboard.js b/dashboard/src/app/dashboard.js index cb1510db..7b89df7d 100644 --- a/dashboard/src/app/dashboard.js +++ b/dashboard/src/app/dashboard.js @@ -1,5 +1,8 @@ class MosquittoDashboard { constructor(headless = false) { + this.abort = new AbortController(); + registerAbortController(this.abort, this); + this.headlessMode = !!headless; !this.headlessMode && window.Chart.register(window.ChartZoom); // chartjs comes from lib/ @@ -1483,7 +1486,10 @@ class MosquittoDashboard { const nowTimestampMilliseconds = new Date().getTime(); let sysTopics = null; try { - sysTopics = await fetchData(SYSTOPIC_ENDPOINT); + sysTopics = await fetchData(SYSTOPIC_ENDPOINT, { + signal: this.abort.signal, + cache: "no-store", + }); this.version = sysTopics?.["$SYS/broker/version"]; this.previousDataFetchFailed = false; @@ -1492,12 +1498,16 @@ class MosquittoDashboard { this.setBrokerStatus(); } catch (error) { const errorMsg = `Error fetching sys topics: ${error?.message}`; - console.error(errorMsg); + if (this.abort.signal.aborted || error?.name === "AbortError") { + console.log("Fetching systopics aborted"); + } else { + console.error(errorMsg); + if (!this.previousDataFetchFailed) { + alert(errorMsg); + } + } this.brokerOnline = false; this.setBrokerStatus(); - if (!this.previousDataFetchFailed) { - alert(errorMsg); - } this.previousDataFetchFailed = true; } if (!sysTopics) { diff --git a/dashboard/src/app/listeners.js b/dashboard/src/app/listeners.js index 58dd327a..ce5e2b03 100644 --- a/dashboard/src/app/listeners.js +++ b/dashboard/src/app/listeners.js @@ -1,15 +1,28 @@ class Listeners { constructor() { + this.abort = new AbortController(); + registerAbortController(this.abort, this); this.init(); } async init() { try { - const listeners = await fetchData(LISTENERS_ENDPOINT); + const listeners = await fetchData(LISTENERS_ENDPOINT, { + signal: this.abort.signal, + cache: "no-store", + }); this.displayListeners(listeners); } catch (error) { - console.error("Error fetching listeners:", error); - alert(`Error loading listeners: ${error}`); + if ( + this.pageHiding || + this.abort.signal.aborted || + error?.name === "AbortError" + ) { + console.log("Fetching listeners aborted"); + } else { + console.error("Error fetching listeners:", error); + alert(`Error loading listeners: ${error}`); + } } } diff --git a/dashboard/src/utils/utils.js b/dashboard/src/utils/utils.js index 3831de0c..b0dadd53 100644 --- a/dashboard/src/utils/utils.js +++ b/dashboard/src/utils/utils.js @@ -19,13 +19,16 @@ function toAsyncAndWaitAfter(task, delay = 0) { }; } -async function fetchData(endpoint) { +async function fetchData(endpoint, opts = {}) { if (!endpoint) { throw new Error("No endpoint provided to fetch data function"); } let data; - const res = await fetch(endpoint); + const res = await fetch(endpoint, { + ...opts, + headers: { Accept: "application/json" }, + }); if (res.ok) { data = await res.json(); } else { @@ -165,3 +168,11 @@ async function copyToClipboard(textToCopy) { function isMobile() { return window.innerWidth < 1024; } + +function registerAbortController(abortController) { + // in firefox the below doesn't help unfortunately: a general netrowk error is being thrown even before the below callback is executed. A proper implementation would require aborying in-flight requets right before the navigation but it's not worth the effort. Currently you will see an alert for a quick moment when spam-clicking onto the "listern" tab in the sidebar on firefox + const abortCallback = () => { + abortController.abort(); + }; + window.addEventListener("pagehide", abortCallback, { once: true }); +}