From db89ea640066ef29cba52e00caa8e0a0cd04db41 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Wed, 30 Sep 2026 21:08:28 +0200 Subject: [PATCH] An upload that did not arrive says why Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt --- CHANGELOG.md | 4 ++++ src/app.rs | 42 +++++++++++++++++++++++++++++++++++++++++- src/i18n.rs | 13 +++++++++++-- src/upload.rs | 2 +- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b381c4..4c9d341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ One line per change, grouped by the release that shipped it. Newest first. A release is a Cargo.toml bump plus a `v` tag; the content repos pin the tag they run (see the end of this file). +## Unreleased + +- An upload that did not arrive says why: larger than 20 MB (checked in the browser before anything is sent), a site that takes no files (no Garage configured), not allowed from here, or anything else. It used to say "try again, or with a smaller file" whatever the reason, which sent people looking for a smaller file when the site had no storage at all. + ## 0.6.7 (2026-09-30) - `leaves_after: { minutes: 10 }` on a state: a record that has been there that long leaves its bucket's lists (days, hours and minutes add up; swept every minute; the event log keeps it). Trainer: `leaves_after` per stage on a need. A Klingenberg Bygg card taken off the front page leaves the office's list ten minutes later instead of standing there greyed out. diff --git a/src/app.rs b/src/app.rs index 6d848f5..8f2510f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1310,6 +1310,12 @@ fn AlternativeCard( }; let Some(files) = input.files() else { continue }; let Some(file) = files.get(0) else { continue }; + // Too big is known before a byte is sent. + if file.size() > MAX_UPLOAD_BYTES as f64 { + send_error.set(Some(t(&lang().get_untracked(), "upload_too_large").to_string())); + sending.set(false); + return; + } match upload_file(&file, &question_id_for_submit, &alt_name_for_submit).await { Ok(key) => { map.insert(name.clone(), serde_json::Value::String(key)); @@ -1319,7 +1325,8 @@ fn AlternativeCard( // say so. Err(e) => { leptos::logging::error!("upload of {name} failed: {e:?}"); - send_error.set(Some(t(&lang().get_untracked(), "upload_failed").to_string())); + let status = e.as_string().and_then(|m| m.rsplit(' ').next().and_then(|n| n.parse().ok())); + send_error.set(Some(t(&lang().get_untracked(), upload_refusal(status)).to_string())); sending.set(false); return; } @@ -2283,6 +2290,24 @@ pub fn uploaded_filename(value: &str) -> Option<&str> { rest.strip_prefix('-').filter(|name| shaped && !name.is_empty()) } +/// The largest file one upload may carry. Here, not in `upload.rs`, +/// so the browser checks the same number before it sends anything. +pub const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024; + +/// Why an upload did not arrive, in words a person can act on, from +/// the status `POST /upload` answered with (see `upload.rs`): too big, +/// a site that takes no files, a page they may not send from, or +/// anything else. +#[cfg_attr(not(feature = "hydrate"), allow(dead_code))] +fn upload_refusal(status: Option) -> &'static str { + match status { + Some(413) => "upload_too_large", + Some(503) => "upload_unavailable", + Some(403) => "upload_forbidden", + _ => "upload_failed", + } +} + /// What a refused server call says, without the wrapping a person /// has no use for. fn server_message(e: &ServerFnError) -> String { @@ -3241,6 +3266,21 @@ mod responsible_tests { mod picture_tests { use super::*; + #[test] + fn an_upload_refusal_says_which_it_was() { + assert_eq!(upload_refusal(Some(413)), "upload_too_large"); + assert_eq!(upload_refusal(Some(503)), "upload_unavailable"); + assert_eq!(upload_refusal(Some(403)), "upload_forbidden"); + assert_eq!(upload_refusal(Some(502)), "upload_failed"); + assert_eq!(upload_refusal(None), "upload_failed"); + for lang in ["en", "no"] { + for status in [Some(413), Some(503), Some(403), None] { + let key = upload_refusal(status); + assert_ne!(crate::i18n::t(lang, key), key, "{lang} has words for {key}"); + } + } + } + #[test] fn a_record_with_a_picture_is_shown_by_it() { assert!(is_picture("Portrett.JPG") && is_picture("a.webp") && !is_picture("rapport.pdf")); diff --git a/src/i18n.rs b/src/i18n.rs index 1b84784..af463f5 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -24,7 +24,10 @@ fn lookup(lang: &str, key: &str) -> Option<&'static str> { ("en", "sign_in") => "Sign in", ("en", "sign_out") => "Sign out", ("en", "nothing_here_yet") => "Nothing here yet.", - ("en", "upload_failed") => "The file could not be sent, so nothing was. Try again, or with a smaller file.", + ("en", "upload_failed") => "The file did not arrive, so nothing was sent. Try again.", + ("en", "upload_too_large") => "The file is larger than 20 MB, so nothing was sent. Choose a smaller one, or a picture taken at a lower resolution.", + ("en", "upload_unavailable") => "This site cannot take files at the moment, so nothing was sent. Let whoever runs the site know.", + ("en", "upload_forbidden") => "Files cannot be sent from here by you, so nothing was sent. Are you signed in?", ("en", "current_file") => "Now:", ("en", "keep_file") => "Choose a new one to replace it, or leave it as it is.", ("en", "nothing_here") => "Nothing here", @@ -57,7 +60,10 @@ fn lookup(lang: &str, key: &str) -> Option<&'static str> { ("no", "sign_in") => "Logg inn", ("no", "sign_out") => "Logg ut", ("no", "nothing_here_yet") => "Ingenting her ennå.", - ("no", "upload_failed") => "Filen kom ikke fram, så ingenting ble sendt. Prøv igjen, eller med en mindre fil.", + ("no", "upload_failed") => "Filen kom ikke fram, så ingenting ble sendt. Prøv igjen.", + ("no", "upload_too_large") => "Filen er større enn 20 MB, så ingenting ble sendt. Velg en mindre fil, eller et bilde med lavere oppløsning.", + ("no", "upload_unavailable") => "Siden kan ikke ta imot filer akkurat nå, så ingenting ble sendt. Si fra til den som har siden.", + ("no", "upload_forbidden") => "Du kan ikke sende filer herfra, så ingenting ble sendt. Er du logget inn?", ("no", "current_file") => "Nå:", ("no", "keep_file") => "Velg en ny for å bytte, eller la den være.", ("no", "nothing_here") => "Ingenting her", @@ -109,6 +115,9 @@ mod tests { "sign_out", "nothing_here_yet", "upload_failed", + "upload_too_large", + "upload_unavailable", + "upload_forbidden", "current_file", "keep_file", "nothing_here", diff --git a/src/upload.rs b/src/upload.rs index da98169..803381f 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -15,7 +15,7 @@ use serde::Serialize; use crate::server::AppState; -pub const MAX_UPLOAD_BYTES: usize = 20 * 1024 * 1024; +pub const MAX_UPLOAD_BYTES: usize = crate::app::MAX_UPLOAD_BYTES; #[derive(Clone)] pub struct Garage {