From 93c4b2e03c5176da01a7e00a03c03ffcce279bc8 Mon Sep 17 00:00:00 2001 From: manx98 <39939386+manx98@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:38:03 +0800 Subject: [PATCH] fix: abort ongoing requests when changing pages (#3927) --- frontend/src/api/files.ts | 32 +++++++++++++++----- frontend/src/api/utils.ts | 9 ++++-- frontend/src/components/Sidebar.vue | 16 ++++++++-- frontend/src/components/prompts/FileList.vue | 21 +++++++++++-- frontend/src/types/api.d.ts | 1 + frontend/src/views/Files.vue | 20 ++++++------ 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts index 0d6a09b7..d9ee12d6 100644 --- a/frontend/src/api/files.ts +++ b/frontend/src/api/files.ts @@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth"; import { useLayoutStore } from "@/stores/layout"; import { baseURL } from "@/utils/constants"; import { upload as postTus, useTus } from "./tus"; -import { createURL, fetchURL, removePrefix } from "./utils"; +import { createURL, fetchURL, removePrefix, StatusError } from "./utils"; -export async function fetch(url: string) { +export async function fetch(url: string, signal?: AbortSignal) { url = removePrefix(url); + const res = await fetchURL(`/api/resources${url}`, { signal }); - const res = await fetchURL(`/api/resources${url}`, {}); - - const data = (await res.json()) as Resource; + let data: Resource; + try { + data = (await res.json()) as Resource; + } catch (e) { + // Check if the error is an intentional cancellation + if (e instanceof Error && e.name === "AbortError") { + throw new StatusError("000 No connection", 0, true); + } + throw e; + } data.url = `/files${url}`; if (data.isDir) { @@ -205,10 +213,18 @@ export function getSubtitlesURL(file: ResourceItem) { return file.subtitles?.map((d) => createURL("api/subtitle" + d, params)); } -export async function usage(url: string) { +export async function usage(url: string, signal: AbortSignal) { url = removePrefix(url); - const res = await fetchURL(`/api/usage${url}`, {}); + const res = await fetchURL(`/api/usage${url}`, { signal }); - return await res.json(); + try { + return await res.json(); + } catch (e) { + // Check if the error is an intentional cancellation + if (e instanceof Error && e.name == "AbortError") { + throw new StatusError("000 No connection", 0, true); + } + throw e; + } } diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts index a5a64fa1..ae509de2 100644 --- a/frontend/src/api/utils.ts +++ b/frontend/src/api/utils.ts @@ -6,7 +6,8 @@ import { encodePath } from "@/utils/url"; export class StatusError extends Error { constructor( message: any, - public status?: number + public status?: number, + public is_canceled?: boolean ) { super(message); this.name = "StatusError"; @@ -33,7 +34,11 @@ export async function fetchURL( }, ...rest, }); - } catch { + } catch (e) { + // Check if the error is an intentional cancellation + if (e instanceof Error && e.name === "AbortError") { + throw new StatusError("000 No connection", 0, true); + } throw new StatusError("000 No connection", 0); } diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 4d55cf0f..8991b571 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -129,6 +129,7 @@ import { import { files as api } from "@/api"; import ProgressBar from "@/components/ProgressBar.vue"; import prettyBytes from "pretty-bytes"; +import { StatusError } from "@/api/utils.js"; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; @@ -136,7 +137,7 @@ export default { name: "sidebar", setup() { const usage = reactive(USAGE_DEFAULT); - return { usage }; + return { usage, usageAbortController: new AbortController() }; }, components: { ProgressBar, @@ -157,6 +158,9 @@ export default { }, methods: { ...mapActions(useLayoutStore, ["closeHovers", "showHover"]), + abortOngoingFetchUsage() { + this.usageAbortController.abort(); + }, async fetchUsage() { const path = this.$route.path.endsWith("/") ? this.$route.path @@ -166,13 +170,18 @@ export default { return Object.assign(this.usage, usageStats); } try { - const usage = await api.usage(path); + this.abortOngoingFetchUsage(); + this.usageAbortController = new AbortController(); + const usage = await api.usage(path, this.usageAbortController.signal); usageStats = { used: prettyBytes(usage.used, { binary: true }), total: prettyBytes(usage.total, { binary: true }), usedPercentage: Math.round((usage.used / usage.total) * 100), }; } catch (error) { + if (error instanceof StatusError && error.is_canceled) { + return; + } this.$showError(error); } return Object.assign(this.usage, usageStats); @@ -200,5 +209,8 @@ export default { immediate: true, }, }, + unmounted() { + this.abortOngoingFetchUsage(); + }, }; diff --git a/frontend/src/components/prompts/FileList.vue b/frontend/src/components/prompts/FileList.vue index 6a10a127..2fd5ec71 100644 --- a/frontend/src/components/prompts/FileList.vue +++ b/frontend/src/components/prompts/FileList.vue @@ -31,6 +31,7 @@ import { useFileStore } from "@/stores/file"; import url from "@/utils/url"; import { files } from "@/api"; +import { StatusError } from "@/api/utils.js"; export default { name: "file-list", @@ -43,6 +44,7 @@ export default { }, selected: null, current: window.location.pathname, + nextAbortController: new AbortController(), }; }, inject: ["$showError"], @@ -56,7 +58,13 @@ export default { mounted() { this.fillOptions(this.req); }, + unmounted() { + this.abortOngoingNext(); + }, methods: { + abortOngoingNext() { + this.nextAbortController.abort(); + }, fillOptions(req) { // Sets the current path and resets // the current items. @@ -94,8 +102,17 @@ export default { // just clicked in and fill the options with its // content. const uri = event.currentTarget.dataset.url; - - files.fetch(uri).then(this.fillOptions).catch(this.$showError); + this.abortOngoingNext(); + this.nextAbortController = new AbortController(); + files + .fetch(uri, this.nextAbortController.signal) + .then(this.fillOptions) + .catch((e) => { + if (e instanceof StatusError && e.is_canceled) { + return; + } + this.$showError(e); + }); }, touchstart(event) { const url = event.currentTarget.dataset.url; diff --git a/frontend/src/types/api.d.ts b/frontend/src/types/api.d.ts index 66685e5e..c1592a21 100644 --- a/frontend/src/types/api.d.ts +++ b/frontend/src/types/api.d.ts @@ -10,6 +10,7 @@ interface ApiOpts { method?: ApiMethod; headers?: object; body?: any; + signal?: AbortSignal; } interface TusSettings { diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue index 39dceeff..370dac53 100644 --- a/frontend/src/views/Files.vue +++ b/frontend/src/views/Files.vue @@ -61,9 +61,7 @@ const route = useRoute(); const { t } = useI18n({}); -const clean = (path: string) => { - return path.endsWith("/") ? path.slice(0, -1) : path; -}; +let fetchDataController = new AbortController(); const error = ref(null); @@ -101,6 +99,7 @@ onUnmounted(() => { layoutStore.toggleShell(); } fileStore.updateRequest(null); + fetchDataController.abort(); }); watch(route, (to, from) => { @@ -142,20 +141,21 @@ const fetchData = async () => { let url = route.path; if (url === "") url = "/"; if (url[0] !== "/") url = "/" + url; + // Cancel the ongoing request + fetchDataController.abort(); + fetchDataController = new AbortController(); try { - const res = await api.fetch(url); - - if (clean(res.path) !== clean(`/${[...route.params.path].join("/")}`)) { - throw new Error("Data Mismatch!"); - } - + const res = await api.fetch(url, fetchDataController.signal); fileStore.updateRequest(res); document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`; + layoutStore.loading = false; } catch (err) { + if (err instanceof StatusError && err.is_canceled) { + return; + } if (err instanceof Error) { error.value = err; } - } finally { layoutStore.loading = false; } };