fix: handle fetch/API errors in telegram delivery to prevent gateway crashes
Wrap all bot.api.sendXxx() media calls in delivery.ts with error handler that logs failures before re-throwing. This ensures network failures are properly logged with context instead of causing unhandled promise rejections that crash the gateway. Also wrap the fetch() call in telegram onboarding with try/catch to gracefully handle network errors during username lookup. Fixes #2487 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,14 +80,20 @@ async function promptTelegramAllowFrom(params: {
|
|||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||||
const res = await fetch(url);
|
try {
|
||||||
const data = (await res.json().catch(() => null)) as {
|
const res = await fetch(url);
|
||||||
ok?: boolean;
|
if (!res.ok) return null;
|
||||||
result?: { id?: number | string };
|
const data = (await res.json().catch(() => null)) as {
|
||||||
} | null;
|
ok?: boolean;
|
||||||
const id = data?.ok ? data?.result?.id : undefined;
|
result?: { id?: number | string };
|
||||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
} | null;
|
||||||
return null;
|
const id = data?.ok ? data?.result?.id : undefined;
|
||||||
|
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
// Network error during username lookup - return null to prompt user for numeric ID
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseInput = (value: string) =>
|
const parseInput = (value: string) =>
|
||||||
|
|||||||
@@ -25,6 +25,24 @@ import type { TelegramContext } from "./types.js";
|
|||||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||||
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a Telegram API call with error logging. Ensures network failures are
|
||||||
|
* logged with context before propagating, preventing silent unhandled rejections.
|
||||||
|
*/
|
||||||
|
async function withMediaErrorHandler<T>(
|
||||||
|
operation: string,
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (err) {
|
||||||
|
const errText = formatErrorMessage(err);
|
||||||
|
runtime.error?.(danger(`telegram ${operation} failed: ${errText}`));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deliverReplies(params: {
|
export async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -146,17 +164,17 @@ export async function deliverReplies(params: {
|
|||||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
mediaParams.message_thread_id = threadParams.message_thread_id;
|
||||||
}
|
}
|
||||||
if (isGif) {
|
if (isGif) {
|
||||||
await bot.api.sendAnimation(chatId, file, {
|
await withMediaErrorHandler("sendAnimation", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendAnimation(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
} else if (kind === "image") {
|
} else if (kind === "image") {
|
||||||
await bot.api.sendPhoto(chatId, file, {
|
await withMediaErrorHandler("sendPhoto", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendPhoto(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
} else if (kind === "video") {
|
} else if (kind === "video") {
|
||||||
await bot.api.sendVideo(chatId, file, {
|
await withMediaErrorHandler("sendVideo", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendVideo(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
} else if (kind === "audio") {
|
} else if (kind === "audio") {
|
||||||
const { useVoice } = resolveTelegramVoiceSend({
|
const { useVoice } = resolveTelegramVoiceSend({
|
||||||
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
|
wantsVoice: reply.audioAsVoice === true, // default false (backward compatible)
|
||||||
@@ -169,9 +187,9 @@ export async function deliverReplies(params: {
|
|||||||
// Switch typing indicator to record_voice before sending.
|
// Switch typing indicator to record_voice before sending.
|
||||||
await params.onVoiceRecording?.();
|
await params.onVoiceRecording?.();
|
||||||
try {
|
try {
|
||||||
await bot.api.sendVoice(chatId, file, {
|
await withMediaErrorHandler("sendVoice", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendVoice(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
} catch (voiceErr) {
|
} catch (voiceErr) {
|
||||||
// Fall back to text if voice messages are forbidden in this chat.
|
// Fall back to text if voice messages are forbidden in this chat.
|
||||||
// This happens when the recipient has Telegram Premium privacy settings
|
// This happens when the recipient has Telegram Premium privacy settings
|
||||||
@@ -204,14 +222,14 @@ export async function deliverReplies(params: {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Audio file - displays with metadata (title, duration) - DEFAULT
|
// Audio file - displays with metadata (title, duration) - DEFAULT
|
||||||
await bot.api.sendAudio(chatId, file, {
|
await withMediaErrorHandler("sendAudio", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendAudio(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await bot.api.sendDocument(chatId, file, {
|
await withMediaErrorHandler("sendDocument", runtime, () =>
|
||||||
...mediaParams,
|
bot.api.sendDocument(chatId, file, { ...mediaParams }),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user