From 6766f564fb4bdde2b6e00ce4fdf679323660ed20 Mon Sep 17 00:00:00 2001 From: Alexander Shamshurin <107514777+Topleess@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:53:33 +0000 Subject: [PATCH] fix: surface dashboard API error messages Extract the dashboard API client into a testable module and display backend-provided error details when requests fail. Fall back to the HTTP status when a response is not JSON. --- test/api-client.test.js | 28 ++++++++++++++++++++++++++++ web/api-client.js | 35 +++++++++++++++++++++++++++++++++++ web/app.js | 20 +------------------- web/index.html | 1 + 4 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 test/api-client.test.js create mode 100644 web/api-client.js diff --git a/test/api-client.test.js b/test/api-client.test.js new file mode 100644 index 0000000..aa8d426 --- /dev/null +++ b/test/api-client.test.js @@ -0,0 +1,28 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { createApiClient } = require("../web/api-client"); + +test("API client surfaces a backend error message", async () => { + const api = createApiClient(async () => ({ + ok: false, + status: 400, + json: async () => ({ error: "TikTok login is already in progress" }), + })); + + await assert.rejects( + api.post("/api/tiktok/login"), + /TikTok login is already in progress/ + ); +}); + +test("API client falls back to the HTTP status for non-JSON errors", async () => { + const api = createApiClient(async () => ({ + ok: false, + status: 502, + json: async () => { + throw new SyntaxError("Unexpected token"); + }, + })); + + await assert.rejects(api.get("/api/status"), /API Error: 502/); +}); diff --git a/web/api-client.js b/web/api-client.js new file mode 100644 index 0000000..1962e78 --- /dev/null +++ b/web/api-client.js @@ -0,0 +1,35 @@ +(function exposeApiClient(root, factory) { + const createApiClient = factory(); + + if (typeof module === "object" && module.exports) { + module.exports = { createApiClient }; + } else { + root.createApiClient = createApiClient; + } +})(typeof globalThis !== "undefined" ? globalThis : this, function apiClientFactory() { + return function createApiClient(fetchImpl = globalThis.fetch) { + async function parse(response) { + const data = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(data?.error || `API Error: ${response.status}`); + } + return data; + } + + return { + async get(endpoint) { + return parse(await fetchImpl(endpoint)); + }, + async post(endpoint, body) { + const options = { + method: "POST", + headers: { "Content-Type": "application/json" }, + }; + if (body !== undefined) { + options.body = JSON.stringify(body); + } + return parse(await fetchImpl(endpoint, options)); + }, + }; + }; +}); diff --git a/web/app.js b/web/app.js index 6a91ee2..9384cd8 100644 --- a/web/app.js +++ b/web/app.js @@ -1,22 +1,4 @@ -const API = { - async get(endpoint) { - const res = await fetch(endpoint); - if (!res.ok) throw new Error(`API Error: ${res.status}`); - return res.json(); - }, - async post(endpoint, body) { - const options = { - method: "POST", - headers: { "Content-Type": "application/json" }, - }; - if (body !== undefined) { - options.body = JSON.stringify(body); - } - const res = await fetch(endpoint, options); - if (!res.ok) throw new Error(`API Error: ${res.status}`); - return res.json(); - }, -}; +const API = createApiClient(); function escapeHtml(value) { return String(value ?? "") diff --git a/web/index.html b/web/index.html index 23d27f0..91f00d7 100644 --- a/web/index.html +++ b/web/index.html @@ -828,6 +828,7 @@

Live Logs

+