From e0025b4cbffc7f2e90b965b1ad2c70e68ff7b094 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 29 Apr 2026 22:43:02 +0100 Subject: [PATCH] Handle oversize upload errors cleanly --- frontend/src/app.ts | 28 +++++++++++++++++++++++++++- src/repixelizer/gui_static/app.js | 20 +++++++++++++++++++- state/evidence.jsonl | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 22759e7..32581a5 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -1568,10 +1568,21 @@ async function startRun(): Promise { setStage("idle", "Waiting for input", "Pick a file first. The machine is not clairvoyant."); return; } + const runtimeConfig = state.runtimeConfig; + const uploadLimit = runtimeConfig?.limits.maxUploadBytes; + if (uploadLimit !== undefined && state.file.size > uploadLimit) { + setJobState("failed"); + setStage( + "failed", + "File too large", + `${state.file.name} is ${formatByteLimit(state.file.size)}. Hosted uploads are capped at ${formatByteLimit(uploadLimit)}.`, + ); + addLog("Failure", "Selected file is over the hosted upload limit."); + return; + } resetRunArtifacts({ preserveSourceImage: true }); renderEventLog(); runButton.disabled = true; - const runtimeConfig = state.runtimeConfig; const maxSteps = runtimeConfig?.limits.maxSteps ?? 48; const defaultSteps = runtimeConfig?.limits.defaultSteps ?? 48; state.solverStepBudget = Math.max(0, Math.min(maxSteps, parseOptionalInteger(stepsInput) ?? defaultSteps)); @@ -1604,6 +1615,21 @@ async function startRun(): Promise { } async function acceptFile(file: File): Promise { + const uploadLimit = state.runtimeConfig?.limits.maxUploadBytes; + if (uploadLimit !== undefined && file.size > uploadLimit) { + state.file = null; + resetRunArtifacts(); + dropzoneLabel.textContent = "Choose image"; + setJobState("failed"); + setStage( + "failed", + "File too large", + `${file.name} is ${formatByteLimit(file.size)}. Hosted uploads are capped at ${formatByteLimit(uploadLimit)}.`, + ); + addLog("Failure", "Selected file is over the hosted upload limit."); + await renderEverything(); + return; + } state.file = file; resetRunArtifacts(); dropzoneLabel.textContent = file.name; diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index e63450d..3fb346b 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -1282,10 +1282,17 @@ async function startRun() { setStage("idle", "Waiting for input", "Pick a file first. The machine is not clairvoyant."); return; } + const runtimeConfig = state.runtimeConfig; + const uploadLimit = runtimeConfig?.limits.maxUploadBytes; + if (uploadLimit !== undefined && state.file.size > uploadLimit) { + setJobState("failed"); + setStage("failed", "File too large", `${state.file.name} is ${formatByteLimit(state.file.size)}. Hosted uploads are capped at ${formatByteLimit(uploadLimit)}.`); + addLog("Failure", "Selected file is over the hosted upload limit."); + return; + } resetRunArtifacts({ preserveSourceImage: true }); renderEventLog(); runButton.disabled = true; - const runtimeConfig = state.runtimeConfig; const maxSteps = runtimeConfig?.limits.maxSteps ?? 48; const defaultSteps = runtimeConfig?.limits.defaultSteps ?? 48; state.solverStepBudget = Math.max(0, Math.min(maxSteps, parseOptionalInteger(stepsInput) ?? defaultSteps)); @@ -1318,6 +1325,17 @@ async function startRun() { } } async function acceptFile(file) { + const uploadLimit = state.runtimeConfig?.limits.maxUploadBytes; + if (uploadLimit !== undefined && file.size > uploadLimit) { + state.file = null; + resetRunArtifacts(); + dropzoneLabel.textContent = "Choose image"; + setJobState("failed"); + setStage("failed", "File too large", `${file.name} is ${formatByteLimit(file.size)}. Hosted uploads are capped at ${formatByteLimit(uploadLimit)}.`); + addLog("Failure", "Selected file is over the hosted upload limit."); + await renderEverything(); + return; + } state.file = file; resetRunArtifacts(); dropzoneLabel.textContent = file.name; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index be318b0..0e85c5b 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -26,3 +26,4 @@ {"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'."} +{"ts": "2026-04-29T21:42:58+00:00", "type": "implementation", "status": "ok", "note": "Oversize uploads were hitting nginx 413 before FastAPI and the GUI masked the response with a double-read error; GUI now preflights file size and reads failed responses once so users see the actual upload limit."}