From 334dcd1edfa7ff1b3fa3cbec483443128d48e5d5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 29 Apr 2026 22:40:14 +0100 Subject: [PATCH] Fix GUI failed response handling --- frontend/src/app.ts | 21 ++++++++++++++++----- src/repixelizer/gui_static/app.js | 21 ++++++++++++++++----- state/evidence.jsonl | 1 + 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 12d9772..22759e7 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -1122,7 +1122,7 @@ function endPan(pointerId: number | null = null): void { async function loadRuntimeConfig(): Promise { const response = await fetch("/api/config"); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to load runtime config.")); } applyRuntimeConfig((await response.json()) as RuntimeConfig); } @@ -1138,7 +1138,7 @@ async function refreshQueueSummary(): Promise { } const response = await fetch("/api/queue"); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to load queue summary.")); } state.queueSummary = (await response.json()) as QueueSummary; renderQueueSummary(); @@ -1223,6 +1223,18 @@ function extractErrorDetail(payload: unknown, fallback: string): string { return fallback; } +async function responseErrorMessage(response: Response, fallback: string): Promise { + const body = await response.text(); + if (!body.trim()) { + return fallback; + } + try { + return extractErrorDetail(JSON.parse(body), fallback); + } catch { + return body; + } +} + async function cancelCurrentJob(options: { keepalive?: boolean; silent?: boolean } = {}): Promise { if (!state.jobId || !hasActiveJob()) { return; @@ -1237,7 +1249,7 @@ async function cancelCurrentJob(options: { keepalive?: boolean; silent?: boolean try { const response = await request; if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to cancel the current job.")); } const payload = (await response.json()) as JobStateSnapshot; applyJobSnapshot(payload); @@ -1573,8 +1585,7 @@ async function startRun(): Promise { body: buildFormData(), }); if (!response.ok) { - const errorPayload = await response.json().catch(async () => ({ detail: await response.text() })); - throw new Error(extractErrorDetail(errorPayload, "Failed to start GUI job.")); + throw new Error(await responseErrorMessage(response, "Failed to start GUI job.")); } const payload = (await response.json()) as JobStateSnapshot; state.jobId = payload.jobId; diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index d7906f1..e63450d 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -887,7 +887,7 @@ function endPan(pointerId = null) { async function loadRuntimeConfig() { const response = await fetch("/api/config"); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to load runtime config.")); } applyRuntimeConfig((await response.json())); } @@ -902,7 +902,7 @@ async function refreshQueueSummary() { } const response = await fetch("/api/queue"); if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to load queue summary.")); } state.queueSummary = (await response.json()); renderQueueSummary(); @@ -979,6 +979,18 @@ function extractErrorDetail(payload, fallback) { } return fallback; } +async function responseErrorMessage(response, fallback) { + const body = await response.text(); + if (!body.trim()) { + return fallback; + } + try { + return extractErrorDetail(JSON.parse(body), fallback); + } + catch { + return body; + } +} async function cancelCurrentJob(options = {}) { if (!state.jobId || !hasActiveJob()) { return; @@ -993,7 +1005,7 @@ async function cancelCurrentJob(options = {}) { try { const response = await request; if (!response.ok) { - throw new Error(await response.text()); + throw new Error(await responseErrorMessage(response, "Failed to cancel the current job.")); } const payload = (await response.json()); applyJobSnapshot(payload); @@ -1287,8 +1299,7 @@ async function startRun() { body: buildFormData(), }); if (!response.ok) { - const errorPayload = await response.json().catch(async () => ({ detail: await response.text() })); - throw new Error(extractErrorDetail(errorPayload, "Failed to start GUI job.")); + throw new Error(await responseErrorMessage(response, "Failed to start GUI job.")); } const payload = (await response.json()); state.jobId = payload.jobId; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 86ed733..be318b0 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -25,3 +25,4 @@ {"ts": "2026-04-29T13:37:17+00:00", "type": "hosted", "status": "accepted", "note": "Hosted Repixelizer now enables Patreon alongside Discord. Patreon access policy is title-only: REPIXELIZER_ACCESS_PATREON_TIER_TITLE=Inner Sanctum is sent to Heimdall as patreon_membership_access during backend-callback OAuth start."} {"ts": "2026-04-29T17:20:00+00:00", "type": "implementation", "status": "ok", "note": "Removed the hosted solver loss chart because the explicit solver's loss-like diagnostic is not a useful live objective; solver status now surfaces step, signal, grid alignment, and displacement terms instead."} {"ts": "2026-04-29T17:29:40+00:00", "type": "implementation", "status": "ok", "note": "Hosted GUI now emits explicit service logs for queue saturation, job queue/start/completion/failure/cancel events, and wait/run/total seconds so load pressure shows up in journald instead of only in transient SSE state."} +{"ts": "2026-04-29T21:40:11+00:00", "type": "implementation", "status": "ok", "note": "Fixed GUI failed-response handling so submit/config/queue/cancel errors read the Response body once, preserving the real server error instead of throwing 'body stream already read'."}