fix(telegram): widen transport-error redaction to all remaining raw exception sites

Extends @AlexFucuson9's 3-site fix (#58594) across the full adapter:
every logger call and SendResult.error that interpolates a raw PTB
exception now routes through _redact_telegram_error_text(). Covers
polling conflict/retry/network ladders, overflow-split edits, draft
sends, prompt/approval/clarify/picker sends, media send fallbacks,
media cache failures, reactions, and chat-info lookups (48 additional
sites). Telegram Bot API exceptions embed the token in the request URL
(/bot<TOKEN>/<method>), so any raw str(exc) is a leak surface.

Adds regression tests for SendResult.error redaction (update prompt,
clarify) and delete_message debug-log redaction.
This commit is contained in:
Teknium 2026-07-16 01:41:26 -07:00
parent 6e96b745d8
commit a04fcbf779
2 changed files with 109 additions and 51 deletions

View file

@ -1222,7 +1222,7 @@ class TelegramAdapter(BasePlatformAdapter):
"retrying without reply/topic anchor: %s",
self.name,
media_label,
send_err,
_redact_telegram_error_text(send_err),
)
if reset_media is not None:
reset_media()
@ -1883,12 +1883,12 @@ class TelegramAdapter(BasePlatformAdapter):
self._rich_draft_disabled = True
logger.debug(
"[%s] sendRichMessageDraft unsupported (%s) — using legacy drafts",
self.name, exc,
self.name, _redact_telegram_error_text(exc),
)
else:
logger.debug(
"[%s] sendRichMessageDraft transient failure (%s) — legacy draft this frame",
self.name, exc,
self.name, _redact_telegram_error_text(exc),
)
return False
@ -2155,7 +2155,7 @@ class TelegramAdapter(BasePlatformAdapter):
if self._polling_error_task and not self._polling_error_task.done():
logger.debug(
"[%s] Telegram polling recovery already scheduled; ignoring %s: %s",
self.name, reason, error,
self.name, reason, _redact_telegram_error_text(error),
)
return
self._send_path_degraded = True
@ -2188,7 +2188,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning(
"[%s] deleteWebhook failed with a recoverable network error; "
"continuing to polling so getUpdates/retry can recover: %s",
self.name, err,
self.name, _redact_telegram_error_text(err),
)
self._send_path_degraded = True
return False
@ -2227,7 +2227,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning(
"[%s] Telegram polling bootstrap conflict; gateway stays alive "
"while conflict retry runs: %s",
self.name, err,
self.name, _redact_telegram_error_text(err),
)
loop = asyncio.get_running_loop()
self._polling_error_task = loop.create_task(self._handle_polling_conflict(err))
@ -2722,7 +2722,7 @@ class TelegramAdapter(BasePlatformAdapter):
"held open on Telegram's servers. Waiting %ds for it to expire. "
"Error: %s",
self.name, self._polling_conflict_count, MAX_CONFLICT_RETRIES,
RETRY_DELAY, error,
RETRY_DELAY, _redact_telegram_error_text(error),
)
# Stop the local updater cleanly before sleeping. If it's already
# stopped (e.g. PTB raised before updater.running was set) this is
@ -2779,7 +2779,7 @@ class TelegramAdapter(BasePlatformAdapter):
"[%s] Telegram polling retry %d/%d failed: %s. "
"Scheduling next attempt.",
self.name, self._polling_conflict_count, MAX_CONFLICT_RETRIES,
retry_err,
_redact_telegram_error_text(retry_err),
)
# Schedule the next retry rather than returning silently.
# Returning here without either restarting polling or setting
@ -2818,7 +2818,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
logger.error(
"[%s] %s Original error: %s",
self.name, message, error,
self.name, message, _redact_telegram_error_text(error),
)
# Snapshot whether we are the call that actually transitions to fatal.
# A concurrent retry task scheduled by an earlier conflict may already
@ -2895,7 +2895,7 @@ class TelegramAdapter(BasePlatformAdapter):
else:
logger.warning(
"[%s] Failed to create DM topic '%s' in chat %s: %s",
self.name, name, chat_id, e,
self.name, name, chat_id, _redact_telegram_error_text(e),
)
return None
@ -3213,7 +3213,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning(
"[%s] Could not register Telegram command menu: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
@ -3600,12 +3600,12 @@ class TelegramAdapter(BasePlatformAdapter):
self._background_tasks.add(self._polling_error_task)
self._polling_error_task.add_done_callback(self._background_tasks.discard)
elif self._looks_like_network_error(error):
logger.warning("[%s] Telegram network error, scheduling reconnect: %s", self.name, error)
logger.warning("[%s] Telegram network _redact_telegram_error_text(error), scheduling reconnect: %s", self.name, error)
self._polling_error_task = loop.create_task(self._handle_polling_network_error(error))
self._background_tasks.add(self._polling_error_task)
self._polling_error_task.add_done_callback(self._background_tasks.discard)
else:
logger.error("[%s] Telegram polling error: %s", self.name, error, exc_info=True)
logger.error("[%s] Telegram polling _redact_telegram_error_text(error): %s", self.name, error, exc_info=True)
# Store reference for retry use in _handle_polling_conflict
self._polling_error_callback_ref = _polling_error_callback
@ -3684,7 +3684,7 @@ class TelegramAdapter(BasePlatformAdapter):
except Exception as e:
logger.debug(
"[%s] Failed to set bot status indicator to %r: %s",
self.name, text, e,
self.name, text, _redact_telegram_error_text(e),
)
async def _cancel_pending_delivery_tasks(self) -> None:
@ -4498,7 +4498,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning(
"[%s] Overflow split: MarkdownV2 first-chunk edit "
"failed, falling back to plain text: %s",
self.name, fmt_err,
self.name, _redact_telegram_error_text(fmt_err),
)
await self._bot.edit_message_text(
chat_id=normalize_telegram_chat_id(chat_id),
@ -4520,9 +4520,9 @@ class TelegramAdapter(BasePlatformAdapter):
else:
logger.error(
"[%s] Overflow split: first-chunk edit failed: %s",
self.name, e, exc_info=True,
self.name, _redact_telegram_error_text(e), exc_info=True,
)
return SendResult(success=False, error=str(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
# Step 2 — send each remaining chunk as a continuation message,
# threaded as a reply to the previous so the user sees them as a
@ -4589,7 +4589,7 @@ class TelegramAdapter(BasePlatformAdapter):
except Exception as _retry_err:
logger.warning(
"[%s] Overflow continuation no-reply retry failed: %s",
self.name, _retry_err,
self.name, _redact_telegram_error_text(_retry_err),
)
sent_msg = None
break
@ -4598,7 +4598,7 @@ class TelegramAdapter(BasePlatformAdapter):
continue
logger.warning(
"[%s] Overflow continuation send failed: %s",
self.name, send_err,
self.name, _redact_telegram_error_text(send_err),
)
sent_msg = None
break
@ -4669,7 +4669,7 @@ class TelegramAdapter(BasePlatformAdapter):
except Exception as e:
logger.debug(
"[%s] Failed to delete Telegram message %s: %s",
self.name, message_id, e,
self.name, message_id, _redact_telegram_error_text(e),
)
return False
@ -4770,14 +4770,14 @@ class TelegramAdapter(BasePlatformAdapter):
logger.debug(
"[%s] sendMessageDraft MarkdownV2 rejected, retrying "
"as plain text (chat=%s draft_id=%s): %s",
self.name, chat_id, draft_id, e,
self.name, chat_id, draft_id, _redact_telegram_error_text(e),
)
continue
logger.debug(
"[%s] sendMessageDraft failed (chat=%s draft_id=%s): %s",
self.name, chat_id, draft_id, e,
)
return SendResult(success=False, error=str(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
return SendResult(success=False, error="draft_rejected")
@ -4860,8 +4860,8 @@ class TelegramAdapter(BasePlatformAdapter):
)
return SendResult(success=True, message_id=str(msg.message_id))
except Exception as e:
logger.warning("[%s] send_update_prompt failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
logger.warning("[%s] send_update_prompt failed: %s", self.name, _redact_telegram_error_text(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
async def send_exec_approval(
self, chat_id: str, command: str, session_key: str,
@ -4939,8 +4939,8 @@ class TelegramAdapter(BasePlatformAdapter):
return SendResult(success=True, message_id=str(msg.message_id))
except Exception as e:
logger.warning("[%s] send_exec_approval failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
logger.warning("[%s] send_exec_approval failed: %s", self.name, _redact_telegram_error_text(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
async def send_slash_confirm(
self, chat_id: str, title: str, message: str, session_key: str,
@ -4987,8 +4987,8 @@ class TelegramAdapter(BasePlatformAdapter):
self._slash_confirm_state[confirm_id] = session_key
return SendResult(success=True, message_id=str(msg.message_id))
except Exception as e:
logger.warning("[%s] send_slash_confirm failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
logger.warning("[%s] send_slash_confirm failed: %s", self.name, _redact_telegram_error_text(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
async def send_clarify(
self,
@ -5069,8 +5069,8 @@ class TelegramAdapter(BasePlatformAdapter):
self._clarify_state[clarify_id] = session_key
return SendResult(success=True, message_id=str(msg.message_id))
except Exception as e:
logger.warning("[%s] send_clarify failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
logger.warning("[%s] send_clarify failed: %s", self.name, _redact_telegram_error_text(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
async def send_model_picker(
self,
@ -5141,8 +5141,8 @@ class TelegramAdapter(BasePlatformAdapter):
return SendResult(success=True, message_id=str(msg.message_id))
except Exception as e:
logger.warning("[%s] send_model_picker failed: %s", self.name, e)
return SendResult(success=False, error=str(e))
logger.warning("[%s] send_model_picker failed: %s", self.name, _redact_telegram_error_text(e))
return SendResult(success=False, error=_redact_telegram_error_text(e))
_PROVIDER_PAGE_SIZE = 10
_MODEL_PAGE_SIZE = 8
@ -6219,7 +6219,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.error(
"[%s] Failed to send Telegram voice/audio, falling back to base adapter: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
return await super().send_voice(chat_id, audio_path, caption, reply_to, metadata=metadata)
@ -6345,7 +6345,7 @@ class TelegramAdapter(BasePlatformAdapter):
except Exception as e:
logger.warning(
"[%s] send_media_group failed (chunk %d/%d), falling back to per-image: %s",
self.name, chunk_idx + 1, len(chunks), e,
self.name, chunk_idx + 1, len(chunks), _redact_telegram_error_text(e),
exc_info=True,
)
# Fallback: send each photo in this chunk individually
@ -6427,7 +6427,7 @@ class TelegramAdapter(BasePlatformAdapter):
"[%s] Failed to send Telegram local image as photo, "
"trying document fallback: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
# Fallback to sending as document (file) — no dimension limit,
@ -6607,7 +6607,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.warning(
"[%s] URL-based send_photo failed, trying file upload: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
# Fallback: download and upload as file (supports up to 10MB)
@ -6691,7 +6691,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.error(
"[%s] Failed to send Telegram animation, falling back to photo: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
# Fallback: try as a regular photo
@ -6778,7 +6778,7 @@ class TelegramAdapter(BasePlatformAdapter):
logger.debug(
"[%s] Failed to send Telegram typing indicator: %s",
self.name,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
@ -6811,7 +6811,7 @@ class TelegramAdapter(BasePlatformAdapter):
"[%s] Failed to get Telegram chat info for %s: %s",
self.name,
chat_id,
e,
_redact_telegram_error_text(e),
exc_info=True,
)
return {"name": str(chat_id), "type": "dm", "error": str(e)}
@ -7503,7 +7503,7 @@ class TelegramAdapter(BasePlatformAdapter):
filename = os.path.basename(getattr(file_obj, "file_path", "") or "")
cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind)
except Exception as exc:
logger.warning("[Telegram] Failed to cache observed group media: %s", exc, exc_info=True)
logger.warning("[Telegram] Failed to cache observed group media: %s", _redact_telegram_error_text(exc), exc_info=True)
return
if cached is None:
@ -7551,7 +7551,7 @@ class TelegramAdapter(BasePlatformAdapter):
filename = os.path.basename(getattr(file_obj, "file_path", "") or "")
cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind)
except Exception as exc:
logger.warning("[Telegram] Failed to cache replied-to media: %s", exc, exc_info=True)
logger.warning("[Telegram] Failed to cache replied-to media: %s", _redact_telegram_error_text(exc), exc_info=True)
return
if cached is None:
@ -7794,7 +7794,7 @@ class TelegramAdapter(BasePlatformAdapter):
self._forum_command_registered.add(chat_id)
logger.info("[%s] Lazy-registered %d commands for forum chat %s", self.name, len(bot_commands), chat_id)
except Exception as e:
logger.warning("[%s] Forum command lazy-registration failed: %s", self.name, e)
logger.warning("[%s] Forum command lazy-registration failed: %s", self.name, _redact_telegram_error_text(e))
def _effective_update_message(self, update: Update) -> Optional[Message]:
"""Return the message-like payload for normal messages and channel posts.
@ -8141,7 +8141,7 @@ class TelegramAdapter(BasePlatformAdapter):
return
except Exception as e:
logger.warning("[Telegram] Failed to cache photo: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache photo: %s", _redact_telegram_error_text(e), exc_info=True)
await self._surface_media_cache_failure(msg, event, "photo", e)
# Download voice/audio messages to cache for STT transcription
@ -8160,7 +8160,7 @@ class TelegramAdapter(BasePlatformAdapter):
event.media_types = ["audio/ogg"]
logger.info("[Telegram] Cached user voice at %s", cached_path)
except Exception as e:
logger.warning("[Telegram] Failed to cache voice: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache voice: %s", _redact_telegram_error_text(e), exc_info=True)
await self._surface_media_cache_failure(msg, event, "voice message", e)
elif msg.audio:
try:
@ -8177,7 +8177,7 @@ class TelegramAdapter(BasePlatformAdapter):
event.media_types = ["audio/mp3"]
logger.info("[Telegram] Cached user audio at %s", cached_path)
except Exception as e:
logger.warning("[Telegram] Failed to cache audio: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache audio: %s", _redact_telegram_error_text(e), exc_info=True)
await self._surface_media_cache_failure(msg, event, "audio file", e)
elif msg.video:
@ -8201,7 +8201,7 @@ class TelegramAdapter(BasePlatformAdapter):
event.media_types = [SUPPORTED_VIDEO_TYPES.get(ext, "video/mp4")]
logger.info("[Telegram] Cached user video at %s", cached_path)
except Exception as e:
logger.warning("[Telegram] Failed to cache video: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache video: %s", _redact_telegram_error_text(e), exc_info=True)
await self._surface_media_cache_failure(msg, event, "video file", e)
# Download document files to cache for agent processing
@ -8248,7 +8248,7 @@ class TelegramAdapter(BasePlatformAdapter):
try:
cached_path = cache_image_from_bytes(bytes(image_bytes), ext=image_ext)
except ValueError as e:
logger.warning("[Telegram] Failed to cache image document: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache image document: %s", _redact_telegram_error_text(e), exc_info=True)
event.text = (
f"Image document '{original_filename or doc_mime or ext or 'unknown'}' "
"could not be read as an image."
@ -8334,7 +8334,7 @@ class TelegramAdapter(BasePlatformAdapter):
pass
except Exception as e:
logger.warning("[Telegram] Failed to cache document: %s", e, exc_info=True)
logger.warning("[Telegram] Failed to cache document: %s", _redact_telegram_error_text(e), exc_info=True)
await self._surface_media_cache_failure(
msg, event, "attachment", e,
display_name=getattr(doc, "file_name", None) or None,
@ -8451,7 +8451,7 @@ class TelegramAdapter(BasePlatformAdapter):
emoji, set_name,
)
except Exception as e:
logger.warning("[Telegram] Sticker analysis error: %s", e, exc_info=True)
logger.warning("[Telegram] Sticker analysis error: %s", _redact_telegram_error_text(e), exc_info=True)
event.text = build_sticker_injection(
f"a sticker with emoji {emoji}" if emoji else "a sticker",
emoji, set_name,
@ -8815,7 +8815,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
return True
except Exception as e:
logger.debug("[%s] set_message_reaction failed (%s): %s", self.name, emoji, e)
logger.debug("[%s] set_message_reaction failed (%s): %s", self.name, emoji, _redact_telegram_error_text(e))
return False
async def _clear_reactions(self, chat_id: str, message_id: str) -> bool:
@ -8836,7 +8836,7 @@ class TelegramAdapter(BasePlatformAdapter):
)
return True
except Exception as e:
logger.debug("[%s] clear reactions failed: %s", self.name, e)
logger.debug("[%s] clear reactions failed: %s", self.name, _redact_telegram_error_text(e))
return False
async def on_processing_start(self, event: MessageEvent) -> None:

View file

@ -130,3 +130,61 @@ async def test_send_video_failure_redacts_token_in_log(monkeypatch, caplog, tmp_
logged = "\n".join(r.getMessage() for r in caplog.records)
assert _SECRET_TOKEN not in logged
assert "Failed to send video" in logged
_SECRET_SEND_URL = f"https://api.telegram.org/bot{_SECRET_TOKEN}/sendMessage"
@pytest.mark.asyncio
async def test_send_update_prompt_failure_redacts_token_in_result_and_log(caplog):
"""A send_update_prompt() transport exception embedding the bot token URL
must not reach the warning log or SendResult.error unredacted."""
adapter = _make_connected_adapter()
adapter._send_message_with_thread_fallback = AsyncMock(
side_effect=RuntimeError(f"Timed out requesting {_SECRET_SEND_URL}")
)
with caplog.at_level("WARNING"):
result = await adapter.send_update_prompt("123", "restart?")
assert result.success is False
assert _SECRET_TOKEN not in (result.error or "")
assert "***" in (result.error or "")
logged = "\n".join(r.getMessage() for r in caplog.records)
assert _SECRET_TOKEN not in logged
@pytest.mark.asyncio
async def test_send_clarify_failure_redacts_token_in_result_and_log(caplog):
"""A send_clarify() transport exception embedding the bot token URL must
not reach the warning log or SendResult.error unredacted."""
adapter = _make_connected_adapter()
adapter._send_message_with_thread_fallback = AsyncMock(
side_effect=RuntimeError(f"Timed out requesting {_SECRET_SEND_URL}")
)
with caplog.at_level("WARNING"):
result = await adapter.send_clarify("123", "q?", ["a", "b"], "cid", "sess")
assert result.success is False
assert _SECRET_TOKEN not in (result.error or "")
logged = "\n".join(r.getMessage() for r in caplog.records)
assert _SECRET_TOKEN not in logged
@pytest.mark.asyncio
async def test_delete_message_failure_redacts_token_in_log(caplog):
"""A delete_message() transport exception embedding the bot token URL
must not reach the debug log unredacted."""
adapter = _make_connected_adapter()
adapter._bot.delete_message = AsyncMock(
side_effect=RuntimeError(f"Bad Request: {_SECRET_SEND_URL}")
)
with caplog.at_level("DEBUG"):
ok = await adapter.delete_message("123", "55")
assert ok is False
logged = "\n".join(r.getMessage() for r in caplog.records)
assert _SECRET_TOKEN not in logged
assert "***" in logged