From dca0c9ab8174480a562c2012f0a07791c9c7c904 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:44:44 -0700 Subject: [PATCH] Fix: fix duplicati widget with no jobs (#7005) --- src/widgets/duplicati/proxy.js | 9 ++++++-- src/widgets/duplicati/proxy.test.js | 32 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/widgets/duplicati/proxy.js b/src/widgets/duplicati/proxy.js index ebf062539..1501a2703 100644 --- a/src/widgets/duplicati/proxy.js +++ b/src/widgets/duplicati/proxy.js @@ -70,7 +70,7 @@ async function login(widget) { return body.AccessToken; } -async function apiGet(widget, endpoint, accessToken) { +async function apiGet(widget, endpoint, accessToken, { allow404 = false } = {}) { const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); const [status, , data] = await httpProxy(url, { method: "GET", @@ -79,6 +79,11 @@ async function apiGet(widget, endpoint, accessToken) { }, }); + // e.g. progressstate 404s until a job has run + if (status === 404 && allow404) { + return null; + } + if (status !== 200) { throw new Error(`Duplicati request failed for ${endpoint}`); } @@ -108,7 +113,7 @@ export default async function duplicatiProxyHandler(req, res) { apiGet(widget, "backups", accessToken), apiGet(widget, "serverstate", accessToken), apiGet(widget, "notifications", accessToken), - apiGet(widget, "progressstate", accessToken), + apiGet(widget, "progressstate", accessToken, { allow404: true }), ]); const summary = buildSummary( diff --git a/src/widgets/duplicati/proxy.test.js b/src/widgets/duplicati/proxy.test.js index 676f27a54..07939635f 100644 --- a/src/widgets/duplicati/proxy.test.js +++ b/src/widgets/duplicati/proxy.test.js @@ -75,6 +75,38 @@ describe("widgets/duplicati/proxy", () => { expect(res.body.lastBackup).toBe("2026-07-12T10:00:00.000Z"); }); + it("tolerates progressstate 404 when no job has run", async () => { + getServiceWidget.mockResolvedValue({ type: "duplicati", url: "http://dup", password: "secret" }); + httpProxy + .mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ AccessToken: "token" }))]) + .mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify([]))]) + .mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ ActiveTask: null }))]) + .mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify([]))]) + .mockResolvedValueOnce([ + 404, + "application/json", + Buffer.from(JSON.stringify({ Error: "No active backup", Code: 404 })), + ]); + + const res = createMockRes(); + await duplicatiProxyHandler({ query: { group: "g", service: "s" } }, res); + + expect(res.statusCode).toBe(200); + expect(res.body.running).toBe(0); + }); + + it("returns 500 when a non-progressstate endpoint 404s", async () => { + getServiceWidget.mockResolvedValue({ type: "duplicati", url: "http://dup", password: "secret" }); + httpProxy + .mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ AccessToken: "token" }))]) + .mockResolvedValue([404, "application/json", Buffer.from("{}")]); + + const res = createMockRes(); + await duplicatiProxyHandler({ query: { group: "g", service: "s" } }, res); + + expect(res.statusCode).toBe(500); + }); + it("returns 500 when login fails", async () => { getServiceWidget.mockResolvedValue({ type: "duplicati", url: "http://dup", password: "secret" }); httpProxy.mockResolvedValueOnce([401, "application/json", Buffer.from("{}")]);