mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-11-03 17:22:20 +01:00
An instance published behind an access list or Cloudflare Zero Trust gets
an HTML login or error page in place of JSON once that session expires.
`resp.json()` then throws "Unexpected token '<', "<!DOCTYPE "...", and the
repeater panes printed that at the user. Diagnosed on the production
server 2026-09-08: the origin had answered a clean 504 JSON ("No reply
from repeater within 10s") and nginx passed it through untouched, so the
HTML could only have come from in front of it.
New window.fetchJson() (fetch-utils.js, loaded from _head_i18n.html so all
eight entry points get it) returns the parsed body — `data.success` and
`data.error` checks are unchanged — and reports anything else in the same
shape the API uses: {success:false, error, http_status, non_json|offline}.
A 401/403 with a non-JSON body is named as an expired login, since that is
the case the user can act on; an error the API sent as JSON keeps its own
message. 88 call sites converted; those that deliberately inspect the
response (resp.ok/status, blob, uploads) still use fetch directly.
Also stops three catch blocks in the repeater panes from rendering the raw
exception text.
Verified in a browser: normal calls unchanged, an HTML answer comes back
as a readable sentence, a refused connection as a network error, and all
seven pages load with no console errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.8 KiB
JavaScript
70 lines
2.8 KiB
JavaScript
/**
|
|
* fetchJson() — fetch + JSON parse with one place to explain a non-JSON answer.
|
|
*
|
|
* Call sites used to do `const resp = await fetch(u); const data = await resp.json();`.
|
|
* That parse throws `SyntaxError: Unexpected token '<', "<!DOCTYPE "...` whenever
|
|
* something in front of the app answers instead of the app: a reverse proxy's
|
|
* auth page, a Cloudflare Access login after the session expires, a gateway
|
|
* error page. Some panes printed that exception at the user verbatim (seen
|
|
* 2026-09-08 on an instance published through Cloudflare Zero Trust — the
|
|
* server itself had answered a clean `{"success": false, ...}` 504, but the
|
|
* expired Access session meant the browser never saw it).
|
|
*
|
|
* The return value is the parsed body, so the usual `data.success` / `data.error`
|
|
* checks keep working unchanged. Anything that goes wrong before that is
|
|
* reported in the same shape the API itself uses, so callers need no new branch:
|
|
*
|
|
* { success: false, error: <translated sentence>, http_status, non_json|offline }
|
|
*
|
|
* An error the API itself sent as JSON is passed through untouched, keeping the
|
|
* server's own message ("No reply from repeater within 10s" and friends).
|
|
*/
|
|
async function fetchJson(url, options) {
|
|
let resp;
|
|
try {
|
|
resp = await fetch(url, options);
|
|
} catch (e) {
|
|
// Connection refused/dropped, DNS, TLS, or a service worker that
|
|
// answered nothing. No status to report.
|
|
return { success: false, offline: true, error: t('common.network_error') };
|
|
}
|
|
|
|
let text;
|
|
try {
|
|
text = await resp.text();
|
|
} catch (e) {
|
|
return { success: false, http_status: resp.status, error: t('common.network_error') };
|
|
}
|
|
|
|
if (text === '') {
|
|
// 204, or an error with no body at all
|
|
if (resp.ok) return { success: true, http_status: resp.status };
|
|
return {
|
|
success: false,
|
|
non_json: true,
|
|
http_status: resp.status,
|
|
error: t('common.error.not_json', { status: resp.status }),
|
|
};
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
// 401/403 is the signature of a login gate in front of the app; other
|
|
// statuses read as a gateway or a wrong URL. Either way the body is a
|
|
// web page, and showing its parse error helps nobody.
|
|
const authGate = resp.status === 401 || resp.status === 403;
|
|
console.error(`fetchJson(${url}): HTTP ${resp.status}, body is not JSON:`,
|
|
text.slice(0, 120));
|
|
return {
|
|
success: false,
|
|
non_json: true,
|
|
http_status: resp.status,
|
|
error: t(authGate ? 'common.error.not_json_auth' : 'common.error.not_json',
|
|
{ status: resp.status }),
|
|
};
|
|
}
|
|
}
|
|
|
|
window.fetchJson = fetchJson;
|