Jelajahi Sumber

[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.
maziggy 5 bulan lalu
induk
melakukan
e149fa23d4
2 mengubah file dengan 9 tambahan dan 4 penghapusan
  1. 1 0
      CHANGELOG.md
  2. 8 4
      backend/app/services/notification_service.py

+ 1 - 0
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.
 - **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.
 - **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.
 - **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
 ### Changed
 
 

+ 8 - 4
backend/app/services/notification_service.py

@@ -217,7 +217,11 @@ class NotificationService:
             return False, "Topic is required"
             return False, "Topic is required"
 
 
         url = f"{server}/{topic}"
         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:
         if auth_token:
             headers["Authorization"] = f"Bearer {auth_token}"
             headers["Authorization"] = f"Bearer {auth_token}"
@@ -229,16 +233,16 @@ class NotificationService:
             # HTTP headers cannot contain newlines, but ntfy interprets
             # HTTP headers cannot contain newlines, but ntfy interprets
             # literal \n (backslash-n) as newlines in the Message header.
             # literal \n (backslash-n) as newlines in the Message header.
             headers["Filename"] = "photo.jpg"
             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)
             response = await client.put(url, content=image_data, headers=headers)
 
 
             if response.status_code == 400 and "attachments not allowed" in response.text:
             if response.status_code == 400 and "attachments not allowed" in response.text:
                 # Server has attachments disabled — retry without the image
                 # Server has attachments disabled — retry without the image
                 headers.pop("Filename", None)
                 headers.pop("Filename", None)
                 headers.pop("Message", 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:
         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):
         if response.status_code in (200, 204):
             return True, "Message sent successfully"
             return True, "Message sent successfully"