mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(settings): guard AI test and regenerate against non-JSON responses
The test and regenerate handlers called res.json() unconditionally. A
proxy-rewritten HTML error page made that throw "Unexpected token '<'",
which leaked into the toast. Both handlers now check the content-type and
wrap the parse, showing a clean message on a non-JSON body. The test
handler also renders the new { ok:false, reason } payload verbatim.
This commit is contained in:
1 parent
564d7dec77
commit
5b5ab5795c
7 files changed
+67
-4
No files matched your search
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Aktiven Provider testen",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test fehlgeschlagen: {message}",
|
||||
"testUnexpectedResponse": "Verbindung zum KI-Anbieter fehlgeschlagen — unerwartete Antwort vom Server.",
|
||||
"disableCoach": {
|
||||
"title": "Coach ausblenden",
|
||||
"description": "Versteckt den Coach-Button und das Coach-Panel auf allen Seiten.",
|
||||
|
||||
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Test active provider",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test failed: {message}",
|
||||
"testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
|
||||
"disableCoach": {
|
||||
"title": "Hide Coach",
|
||||
"description": "Hides the Coach button and drawer everywhere.",
|
||||
|
||||
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Test active provider",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test failed: {message}",
|
||||
"testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
|
||||
"disableCoach": {
|
||||
"title": "Ocultar el Coach",
|
||||
"description": "Oculta el botón del Coach y el panel en todas las páginas.",
|
||||
|
||||
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Test active provider",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test failed: {message}",
|
||||
"testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
|
||||
"disableCoach": {
|
||||
"title": "Masquer le Coach",
|
||||
"description": "Masque le bouton et le panneau du Coach sur toutes les pages.",
|
||||
|
||||
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Test active provider",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test failed: {message}",
|
||||
"testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
|
||||
"disableCoach": {
|
||||
"title": "Nascondi il Coach",
|
||||
"description": "Nasconde il pulsante e il pannello del Coach su tutte le pagine.",
|
||||
|
||||
@@ -3257,6 +3257,7 @@
|
||||
"testProvider": "Test active provider",
|
||||
"testSuccess": "OK — {provider} ({model})",
|
||||
"testFailedShort": "Test failed: {message}",
|
||||
"testUnexpectedResponse": "AI provider connection failed — unexpected response from the server.",
|
||||
"disableCoach": {
|
||||
"title": "Ukryj Coacha",
|
||||
"description": "Ukrywa przycisk Coacha i panel na wszystkich stronach.",
|
||||
|
||||
@@ -1736,7 +1736,33 @@ function RuntimeActionsRow({
|
||||
setTestMsg(null);
|
||||
try {
|
||||
const res = await fetch("/api/ai/test", { method: "POST" });
|
||||
const json = await res.json();
|
||||
// Guard against a non-JSON body. A reverse proxy / Cloudflare can
|
||||
// rewrite an origin error to its own HTML page; parsing that as JSON
|
||||
// throws `Unexpected token '<'` (Safari: "did not match the
|
||||
// expected pattern"). Show a clean message instead of leaking that.
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
setTestOk(false);
|
||||
setTestMsg(t("settings.ai.testUnexpectedResponse"));
|
||||
return;
|
||||
}
|
||||
let json: {
|
||||
data?: {
|
||||
ok?: boolean;
|
||||
providerType?: string;
|
||||
model?: string;
|
||||
reason?: string;
|
||||
} | null;
|
||||
error?: string | null;
|
||||
};
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
setTestOk(false);
|
||||
setTestMsg(t("settings.ai.testUnexpectedResponse"));
|
||||
return;
|
||||
}
|
||||
// 4xx config errors still arrive via the error envelope.
|
||||
if (!res.ok) {
|
||||
setTestOk(false);
|
||||
setTestMsg(
|
||||
@@ -1746,11 +1772,22 @@ function RuntimeActionsRow({
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Provider-call failures now arrive as 200 + { ok:false, reason }
|
||||
// so the body is never rewritten by a proxy. Show the reason
|
||||
// verbatim — it is a categorised, secret-free string.
|
||||
if (json.data && json.data.ok === false) {
|
||||
setTestOk(false);
|
||||
setTestMsg(
|
||||
json.data.reason ??
|
||||
t("settings.ai.testFailedShort", { message: `HTTP ${res.status}` }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setTestOk(true);
|
||||
setTestMsg(
|
||||
t("settings.ai.testSuccess", {
|
||||
provider: json.data.providerType,
|
||||
model: json.data.model,
|
||||
provider: json.data?.providerType ?? "",
|
||||
model: json.data?.model ?? "",
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -1774,7 +1811,27 @@ function RuntimeActionsRow({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ force: true }),
|
||||
});
|
||||
const json = await res.json();
|
||||
// Same defensive guard as the connection test: a proxy can rewrite
|
||||
// a 5xx (e.g. all-providers-failed) to an HTML page, which would
|
||||
// crash `res.json()` with `Unexpected token '<'`.
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
setRegenOk(false);
|
||||
setRegenMsg(
|
||||
res.status === 429
|
||||
? t("settings.regenerateRateLimit")
|
||||
: t("settings.ai.testUnexpectedResponse"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let json: { error?: string | null };
|
||||
try {
|
||||
json = await res.json();
|
||||
} catch {
|
||||
setRegenOk(false);
|
||||
setRegenMsg(t("settings.ai.testUnexpectedResponse"));
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
setRegenOk(false);
|
||||
setRegenMsg(
|
||||
|
||||
Reference in new issue
Block a user