From e149fa23d43a878abdb67ff826129a03d922cd0b Mon Sep 17 00:00:00 2001 From: maziggy Date: Wed, 18 Mar 2026 09:39:46 +0100 Subject: [PATCH] [Fix] Ntfy notifications fail with non-ASCII printer names (#742) Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters. httpx enforces ASCII encoding on string header values, but the Title and Message headers can contain printer names with accented letters or CJK characters. Encode these header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. --- CHANGELOG.md | 1 + backend/app/services/notification_service.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422940215..771e1df6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to Bambuddy will be documented in this file. - **Webhook Notifications Missing Camera Snapshot** ([#679](https://github.com/maziggy/bambuddy/issues/679)) — Webhook notification providers did not include camera snapshots (e.g. from First Layer Complete notifications), even though providers like Telegram, Pushover, ntfy, and Discord already attached them. The webhook payload now includes a base64-encoded `image` field when a snapshot is available (generic format only, not Slack format). Reported by @Arn0uDz. - **Mobile Sidebar Not Scrollable** — On mobile devices with many navigation items, the sidebar did not scroll, making bottom items unreachable. Added overflow scrolling to the nav section while keeping the logo and footer pinned. - **User Notification Ruff/Lint Fixes** ([#693](https://github.com/maziggy/bambuddy/pull/693)) — Fixed missing `timezone` import in email timestamp, unused lambda argument, PEP 8 blank line spacing for `mark_printer_stopped_by_user`, and SQLAlchemy forward reference in `UserEmailPreference` model. +- **Ntfy Notifications Fail With Non-ASCII Characters** ([#742](https://github.com/maziggy/bambuddy/issues/742)) — Ntfy notifications with camera snapshots failed when the printer name or filename contained non-ASCII characters (e.g. accented letters, CJK). The `Title` and `Message` HTTP headers were passed as Python strings, causing httpx to reject them with `UnicodeEncodeError`. Fixed by encoding header values as UTF-8 bytes, which ntfy handles correctly. Test notifications were unaffected because they use a hardcoded ASCII title and no image attachment. Reported by @user. ### Changed diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py index 340ff9c3f..fd394d949 100644 --- a/backend/app/services/notification_service.py +++ b/backend/app/services/notification_service.py @@ -217,7 +217,11 @@ class NotificationService: return False, "Topic is required" url = f"{server}/{topic}" - headers = {"Title": title} + # ntfy reads Title/Message from HTTP headers. httpx enforces ASCII + # for str header values, but printer names and filenames can contain + # non-ASCII characters (e.g. accented letters, CJK). Passing bytes + # bypasses the ASCII check — ntfy handles UTF-8 headers correctly. + headers: dict[str, str | bytes] = {"Title": title.encode("utf-8")} if auth_token: headers["Authorization"] = f"Bearer {auth_token}" @@ -229,16 +233,16 @@ class NotificationService: # HTTP headers cannot contain newlines, but ntfy interprets # literal \n (backslash-n) as newlines in the Message header. headers["Filename"] = "photo.jpg" - headers["Message"] = message.replace("\n", "\\n") + headers["Message"] = message.replace("\n", "\\n").encode("utf-8") response = await client.put(url, content=image_data, headers=headers) if response.status_code == 400 and "attachments not allowed" in response.text: # Server has attachments disabled — retry without the image headers.pop("Filename", None) headers.pop("Message", None) - response = await client.post(url, content=message, headers=headers) + response = await client.post(url, content=message.encode("utf-8"), headers=headers) else: - response = await client.post(url, content=message, headers=headers) + response = await client.post(url, content=message.encode("utf-8"), headers=headers) if response.status_code in (200, 204): return True, "Message sent successfully"