From be9a2de31a6babc94f415bc893b7a4f152165aba Mon Sep 17 00:00:00 2001 From: larssand Date: Wed, 9 Sep 2026 12:06:58 +0200 Subject: [PATCH] Explain a Backend URL/page-origin mismatch instead of a bare fetch error A real Windows install had sync and backups fail everywhere with "Failed to fetch" while the server itself was fully healthy - confirmed via Invoke-WebRequest returning 200 for every endpoint the browser couldn't reach. The page was opened via a LAN IP while Settings still had the Backend URL set to 127.0.0.1 from an earlier session; browsers treat those as different origins, so the server's CORS check correctly rejected the requests, but a CORS rejection surfaces to fetch() as an unexplained generic error with no indication of the real cause. Settings now detects this mismatch on every render and shows the page's actual address with a one-click button to fix it. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 ++++++ ROADMAP.md | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- src/app.js | 4 ++++ src/settings.js | 14 ++++++++++++++ src/styles.css | 22 ++++++++++++++++++++++ 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e8da9..718b95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The application version is maintained in `package.json` and displayed in the adm No changes recorded yet. +## 1.4.2 - 2026-09-09 + +### Fixed + +- Sync and backups could fail everywhere with an unexplained "Failed to fetch", while the server itself was completely healthy, whenever the page was opened via a different address (e.g. a LAN IP) than the one saved in Settings under "Backend URL" - browsers treat those as different origins, and the browser's own security check (CORS) silently blocks the requests without explaining why. Settings now detects this mismatch, names the page's actual address, and offers a one-click button to fix it. + ## 1.4.1 - 2026-09-09 ### Fixed diff --git a/ROADMAP.md b/ROADMAP.md index dfd4481..77c551b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -46,8 +46,11 @@ Acceptance: users can distinguish server contact, pending edits, an active save - [x] Offer manual retry for unsaved changes without discarding local edits. - [x] Verify save failure/reconnection in a real browser. - [x] Extend stale-data feedback to the personal mobile profile: last applied retrieval, 30-second stale warning, timeout and Retry without discarding loaded data. +- [x] Detect and explain a Backend URL/page-origin mismatch, the real cause behind an otherwise unexplained "Failed to fetch" on every sync and backup. - [ ] Review concurrent writes, reconnect recovery and multiple controllers editing the same race. +Fixed 2026-09-09: a real Windows install showed "Backup failed: Sync failed: Failed to fetch" and every other backend call failing the same way, while the server itself was healthy (confirmed via `Invoke-WebRequest` returning 200 for `/api/health`, `/api/state` and `/api/backups`) - only the browser's own `fetch()` calls failed. Root cause: the page was opened via the LAN IP (`http://192.168.1.100:8081`) while the persisted Settings "Backend URL" still said `http://127.0.0.1:8081` from an earlier session. Browsers treat those as different origins even on the same machine, so the server's CORS check (`server.js`, comparing the request's `Origin` header against its own `Host` header) correctly rejected it - but nothing told the user why, since a CORS-rejected response surfaces to `fetch()` as a generic "Failed to fetch" with no further detail, and a command-line tool like `Invoke-WebRequest` doesn't send an `Origin` header at all, so it never reproduced the symptom. The Settings page now compares `window.location.origin` against the configured Backend URL on every render and, when they differ, shows a plain-language warning naming the page's actual address plus a one-click "Use `
`" button that fixes it immediately. Verified with a headless-browser check (seeding a deliberately mismatched Backend URL, confirming the warning text names the real origin, and confirming the fix button both clears the warning and persists the corrected value to the backend) against an isolated temp SQLite instance. + ### Implemented: decoder/socket diagnostics Acceptance: each configured channel distinguishes an open socket from actual received data and successfully parsed passing messages. Inspecting diagnostics must not interrupt timing or rerender an active settings form. diff --git a/package-lock.json b/package-lock.json index f4ba333..49f3963 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rc-racecore", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rc-racecore", - "version": "1.4.1", + "version": "1.4.2", "hasInstallScript": true, "dependencies": { "better-sqlite3": "^12.11.1", diff --git a/package.json b/package.json index 55d125c..40219cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-racecore", - "version": "1.4.1", + "version": "1.4.2", "private": true, "description": "RC RaceCore live timing and race control with AMMC ingest and SQLite persistence", "engines": { diff --git a/src/app.js b/src/app.js index bfd201a..03f93ea 100644 --- a/src/app.js +++ b/src/app.js @@ -1301,6 +1301,8 @@ const TRANSLATIONS = { "settings.pit_role_hint": "Kräver två extra fysiska decoders: en vid depå in och en vid depå ut.", "settings.connection_title": "Backend och anslutning", "settings.backend_url_hint": "Webb/API på datorn som kör npm start, normalt port 8081.", + "settings.backend_url_mismatch": "Sidan är öppnad via {origin}, men Backend URL nedan är en annan adress. Webbläsaren blockerar då synk och backup (CORS) - de måste vara identiska.", + "settings.backend_url_use_current": "Använd {origin}", "settings.managed_channels_hint": "Fälten Decoder IP / host nedan är de fysiska decodrarnas adresser. Backend startar en AMMC-process per roll på unika portar: main 9000, mellantid 9001, depå in 9002 och depå ut 9003.", "settings.pit_entry_ws_url": "Depå in WebSocket-url", "settings.pit_exit_ws_url": "Depå ut WebSocket-url", @@ -2713,6 +2715,8 @@ const TRANSLATIONS = { "settings.pit_role_hint": "Requires two additional physical decoders: one at pit entry and one at pit exit.", "settings.connection_title": "Backend and connection", "settings.backend_url_hint": "Web/API on the computer running npm start, normally port 8081.", + "settings.backend_url_mismatch": "This page was opened via {origin}, but the Backend URL below is a different address. The browser will block sync and backups (CORS) - they must be identical.", + "settings.backend_url_use_current": "Use {origin}", "settings.managed_channels_hint": "Decoder IP / host fields below are physical decoder addresses. The backend starts one AMMC process per role on unique ports: main 9000, split 9001, pit entry 9002, and pit exit 9003.", "settings.pit_entry_ws_url": "Pit entry WebSocket URL", "settings.pit_exit_ws_url": "Pit exit WebSocket URL", diff --git a/src/settings.js b/src/settings.js index 0a6eeb9..f601995 100644 --- a/src/settings.js +++ b/src/settings.js @@ -70,6 +70,9 @@ export function renderSettings(deps) { await action(); return true; }; + const pageOrigin = /^https?:$/.test(window.location.protocol) ? window.location.origin : ""; + const configuredBackendUrl = String(state.settings.backendUrl || getDefaultBackendUrl()).replace(/\/+$/, ""); + const backendUrlMismatch = Boolean(pageOrigin && configuredBackendUrl && pageOrigin !== configuredBackendUrl); const ammcStatus = ammc.status || {}; const ammcOutput = Array.isArray(ammcStatus.lastOutput) ? ammcStatus.lastOutput : []; const suggestedWsUrl = getManagedWsUrl("main"); @@ -181,6 +184,12 @@ export function renderSettings(deps) { state.settings.backendUrl || getDefaultBackendUrl() )}" placeholder="http://127.0.0.1:8081" /> + ${backendUrlMismatch ? ` +

+ ${escapeHtml(t("settings.backend_url_mismatch", { origin: pageOrigin }))} + +

+ ` : ""}