fix(telegram): retry sends without stale thread id#30762
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Telegram topic/thread fallback can misdeliver replies to parent chat when thread ID is stale
DescriptionThe new
When the thread/topic ID is stale (deleted topic, wrong stored thread ID, etc.), the new fallback will resend the same content to the parent chat (no topic), which can cause unintended message delivery:
Vulnerable behavior: // if thread not found -> retry without message_thread_id
return await send(removeMessageThreadIdParam(requestParams));This is a behavior change from “fail closed” (no message delivered) to “fail open” (deliver to a different location) for forum-topic scoped sends. RecommendationDo not automatically fall back to a threadless send for contexts where Safer options:
Example (restrict fallback to DMs): async function sendTelegramWithThreadFallback<T>(params: {
operation: string;
runtime: RuntimeEnv;
requestParams?: Record<string, unknown>;
allowThreadlessFallback?: boolean;
send: (effectiveParams?: Record<string, unknown>) => Promise<T>;
}) {
try {
return await params.send(params.requestParams);
} catch (err) {
if (!params.allowThreadlessFallback) throw err;
if (!hasMessageThreadIdParam(params.requestParams) || !isTelegramThreadNotFoundError(err)) throw err;
return await params.send(removeMessageThreadIdParam(params.requestParams));
}
}
// call site (delivery.ts):
// allowThreadlessFallback: thread?.scope === "dm"This preserves the DM reliability improvement while preventing cross-topic misdelivery in forum/group contexts. Analyzed PR: #30762 at commit |
Retry Telegram bot outbound sends once without message_thread_id when Telegram returns "message thread not found", covering both text and media paths. This prevents DM/topic thread metadata mismatches from causing silent post-image delivery failures.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 172b1b3d55
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!hasMessageThreadIdParam(requestParams) || !isTelegramThreadNotFoundError(err)) { | ||
| throw err; |
There was a problem hiding this comment.
Restrict threadless retry to DM topics only
withTelegramThreadFallback retries without message_thread_id for any thread-not-found error, but deliverReplies is used for forum chats too (resolveTelegramThreadSpec can set scope: "forum"). In a forum supergroup, removing message_thread_id reroutes the send to the General topic, so a stale/deleted topic can cause replies to be posted in the wrong thread instead of failing closed; this is a cross-topic message leak introduced by the new fallback path.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR introduces a retry mechanism for Telegram bot outbound sends (text and all media types) that automatically drops the stale Key findings:
Confidence Score: 3/5
Last reviewed commit: 8cc1eeb |
| async function sendTelegramWithThreadFallback<T>(params: { | ||
| operation: string; | ||
| runtime: RuntimeEnv; | ||
| requestParams?: Record<string, unknown>; | ||
| shouldLog?: (err: unknown) => boolean; | ||
| send: (effectiveParams?: Record<string, unknown>) => Promise<T>; | ||
| }): Promise<T> { | ||
| return await withTelegramThreadFallback({ | ||
| requestParams: params.requestParams, | ||
| runtime: params.runtime, | ||
| operation: params.operation, | ||
| send: async (effectiveParams) => | ||
| await withTelegramApiErrorLogging({ | ||
| operation: params.operation, | ||
| runtime: params.runtime, | ||
| ...(params.shouldLog ? { shouldLog: params.shouldLog } : {}), | ||
| fn: () => params.send(effectiveParams), | ||
| }), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Thread-not-found error is logged as a danger before the retry succeeds
In sendTelegramWithThreadFallback, the send callback wraps every call in withTelegramApiErrorLogging. When the first attempt fails with "message thread not found", withTelegramApiErrorLogging evaluates shouldLog(err) — which returns true for this error in both the HTML path (only excludes parse/empty-text errors) and for media sends (no shouldLog at all, so it defaults to always logging). This causes runtime.error to be called with danger("telegram sendMessage/sendPhoto/… failed: 400: Bad Request: message thread not found") before withTelegramThreadFallback even gets the chance to retry. The message ultimately delivers successfully, but a false danger-level error is emitted every time the retry path is taken.
This can trigger spurious error alerts in monitoring for a condition that is fully handled. Consider excluding the thread-not-found error from logging on the first attempt (e.g., by adding THREAD_NOT_FOUND_RE to the shouldLog guards in the callers, or adjusting the layering so the error is only logged if the retry also fails).
As written, the retry tests also do not assert expect(runtime.error).not.toHaveBeenCalled(), so this false-error behaviour is not caught by the new test suite.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/bot/delivery.ts
Line: 96-115
Comment:
**Thread-not-found error is logged as a `danger` before the retry succeeds**
In `sendTelegramWithThreadFallback`, the `send` callback wraps every call in `withTelegramApiErrorLogging`. When the first attempt fails with "message thread not found", `withTelegramApiErrorLogging` evaluates `shouldLog(err)` — which returns `true` for this error in both the HTML path (only excludes parse/empty-text errors) and for media sends (no `shouldLog` at all, so it defaults to always logging). This causes `runtime.error` to be called with `danger("telegram sendMessage/sendPhoto/… failed: 400: Bad Request: message thread not found")` before `withTelegramThreadFallback` even gets the chance to retry. The message ultimately delivers successfully, but a false `danger`-level error is emitted every time the retry path is taken.
This can trigger spurious error alerts in monitoring for a condition that is fully handled. Consider excluding the thread-not-found error from logging on the first attempt (e.g., by adding `THREAD_NOT_FOUND_RE` to the `shouldLog` guards in the callers, or adjusting the layering so the error is only logged if the retry also fails).
As written, the retry tests also do not assert `expect(runtime.error).not.toHaveBeenCalled()`, so this false-error behaviour is not caught by the new test suite.
How can I resolve this? If you propose a fix, please make it concise.
Summary
message_thread_idwhen Telegram returnsmessage thread not foundphoto/video/audio/voice/document/animation)src/telegram/bot/delivery.test.tsTest plan
pnpm exec vitest run src/telegram/bot/delivery.test.tsCloses #30674.