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"