From 5a4112f41ff84ec5f11cf3633370734bcb63b130 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 21:50:21 +0200 Subject: [PATCH] Dashboard writes were silently GETs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishing a notice did nothing and said it had worked. `api()` took only `path` and dropped the options object every caller passes, so `api(path, {method: "POST", body: ...})` fetched the URL. The read succeeded -- notices are publicly listable -- so the page printed "zveřejněno" over a collection that had not changed. Switching a notice on or off is a PATCH and went the same way. It takes method and body now, and reports what PocketBase said rather than a bare status, so a permissions problem cannot look like an outage. Found because the owner tried to tell the family that the anime tab is down -- AniList have disabled their API, and every call returns 403 with "The AniList API has been temporarily disabled due to severe stability issues" -- and the notice never reached anyone. --- pb_public/status.html | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/pb_public/status.html b/pb_public/status.html index 3ddebe3..a57e8b2 100644 --- a/pb_public/status.html +++ b/pb_public/status.html @@ -206,10 +206,33 @@ function statusPill(s){ return '' + esc(t) + ""; } -async function api(path){ - var r = await fetch(path, { headers: { Authorization: tok } }); +// Takes the options its callers have always passed. +// +// It used to accept only `path`, so every write on this page -- publishing a +// notice, switching one off -- was quietly performed as a GET. The read +// succeeded (the collection is publicly listable), so the page reported +// "zveřejněno" and nothing had been written. A notice about the anime tab being +// down never reached anyone. +async function api(path, opts){ + opts = opts || {}; + var init = { method: opts.method || "GET", headers: { Authorization: tok } }; + if (opts.body !== undefined) { + init.headers["Content-Type"] = "application/json"; + init.body = JSON.stringify(opts.body); + } + var r = await fetch(path, init); if (r.status === 401) { logout(); throw new Error("401"); } - if (!r.ok) throw new Error("HTTP " + r.status); + if (!r.ok) { + // PocketBase explains itself in the body; say what it said rather than a + // bare status, or a permission problem looks the same as an outage. + var detail = ""; + try { + var j = await r.json(); + if (j && j.message) detail = ": " + j.message; + } catch (_) {} + throw new Error("HTTP " + r.status + detail); + } + if (r.status === 204) return null; return r.json(); }