Picture: surface the real reason a URL download fails

Couldn't reproduce the reported URL failing -- downloads fine from
here (200, valid 2.5MB PNG, verified through the actual running app
too) -- but found the error handling was swallowing the real
exception either way: network failure and "not a valid image" both
landed on the same generic message, and nothing was logged, so there
was no way to tell which one it actually was, from the Pi or from
here.

Now: timeouts, connection/HTTP errors, and decode failures each get
their own specific error message, and failures get logged via the
standard logger (visible in journalctl -u paperwall.service). Also
bumped the download timeout 15s -> 30s -- it's a one-time save-time
download now, not a recurring poll, so there's no real cost to being
more patient with a slow host or a large image.

Verified against the real app: the exact reported URL, a non-image
URL, and an unresolvable domain each now produce distinct, logged
error messages instead of one generic one.
This commit is contained in:
Honey committed 2026-07-26 22:28:37 +02:00
1 parent 9aacd4936a
commit 474f8640fa
1 file changed
+20 -4
+20 -4
View File
@@ -1,5 +1,6 @@
"""Flask app: status/preview page + one settings form per screen + rotation settings."""
import logging
from io import BytesIO
import requests
@@ -9,9 +10,11 @@ from werkzeug.utils import secure_filename
from screens.picture import UPLOAD_DIR
logger = logging.getLogger(__name__)
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "bmp", "webp"}
PIL_FORMAT_TO_EXT = {"JPEG": "jpg", "PNG": "png", "GIF": "gif", "BMP": "bmp", "WEBP": "webp"}
PICTURE_URL_TIMEOUT = 15
PICTURE_URL_TIMEOUT = 30 # a one-time download at save-time, not a recurring poll -- fine to be generous
MAX_SCHEDULE_RULES = 4
PICTURE_URL_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
@@ -244,12 +247,25 @@ def create_app(config, screens: dict, scheduler) -> Flask:
response = requests.get(url, timeout=PICTURE_URL_TIMEOUT, headers=PICTURE_URL_HEADERS)
response.raise_for_status()
content = response.content
fmt = Image.open(BytesIO(content)).format
except (requests.exceptions.RequestException, OSError):
fmt = None
except requests.exceptions.Timeout:
content = fmt = None
error = f"Timed out downloading that URL after {PICTURE_URL_TIMEOUT}s -- the host might be slow or blocking this server."
except requests.exceptions.RequestException as exc:
content = fmt = None
logger.warning("Picture URL download failed for %s: %s", url, exc)
error = f"Could not download that URL -- {exc}"
else:
try:
fmt = Image.open(BytesIO(content)).format
except OSError as exc:
logger.warning("Picture URL %s downloaded but didn't decode as an image: %s", url, exc)
fmt = None
ext = PIL_FORMAT_TO_EXT.get(fmt or "")
if not ext:
error = "Could not download a valid image from that URL -- try a different link, or upload a file instead."
if not error:
error = "That URL doesn't look like a valid image -- try a different link, or upload a file instead."
else:
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# Only one picture is ever active -- clear any previous