refactor(signal): wrap top-level receive handler with _safe_handle

Replace the inline try/except at the end of _handle_receive_notification
with a small async context manager that swallows the exception, logs
self.logger.error with the offending payload's repr (bounded to 200 chars),
and attaches the traceback via logger.opt(exception=True).

The previous log line only carried `e`, so diagnosing a bad envelope from
production logs required correlating timestamps. The wrapper is generic so
future receive/dispatch sites can adopt it; for now only this site uses it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kaloyan Tenchov 2026-05-16 11:30:53 -04:00 committed by chengyongru
parent 1377759705
commit 971b774282

View File

@ -8,7 +8,8 @@ import re
import shutil import shutil
import unicodedata import unicodedata
from collections import deque from collections import deque
from collections.abc import Callable from collections.abc import AsyncIterator, Callable
from contextlib import asynccontextmanager
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -557,10 +558,29 @@ class SignalChannel(BaseChannel):
self.logger.error(f"Error in SSE receive loop: {e}") self.logger.error(f"Error in SSE receive loop: {e}")
raise raise
@asynccontextmanager
async def _safe_handle(
self, action: str, payload: Any = None
) -> AsyncIterator[None]:
"""Swallow and log any exception from a top-level handler block.
Logs `self.logger.error` with the action name, the exception, and a
bounded ``repr`` of the offending payload so the offending input is
recoverable from logs without having to correlate by timestamp.
"""
try:
yield
except Exception as e:
snippet = repr(payload)[:200] if payload is not None else ""
text = f"Error in {action}: {e}"
if snippet:
text += f" | payload={snippet}"
self.logger.opt(exception=True).error(text)
async def _handle_receive_notification(self, params: dict[str, Any]) -> None: async def _handle_receive_notification(self, params: dict[str, Any]) -> None:
"""Handle incoming message notification from signal-cli.""" """Handle incoming message notification from signal-cli."""
self.logger.debug(f"_handle_receive_notification called with: {params}") self.logger.debug(f"_handle_receive_notification called with: {params}")
try: async with self._safe_handle("receive notification", params):
# Extract envelope from SSE notification: {"envelope": {...}} # Extract envelope from SSE notification: {"envelope": {...}}
envelope = params.get("envelope", {}) envelope = params.get("envelope", {})
@ -613,9 +633,6 @@ class SignalChannel(BaseChannel):
elif typing_message: elif typing_message:
pass # Ignore typing indicators pass # Ignore typing indicators
except Exception as e:
self.logger.error(f"Error handling receive notification: {e}")
async def _handle_data_message( async def _handle_data_message(
self, self,
sender_id: str, sender_id: str,