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;
|
||||
const username = stripped.startsWith("@") ? stripped : `@${stripped}`;
|
||||
const url = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(username)}`;
|
||||
const res = await fetch(url);
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | null;
|
||||
const id = data?.ok ? data?.result?.id : undefined;
|
||||
if (typeof id === "number" || typeof id === "string") return String(id);
|
||||
return null;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json().catch(() => null)) as {
|
||||
ok?: boolean;
|
||||
result?: { id?: number | string };
|
||||
} | 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) =>
|
||||
|
||||
@@ -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 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: {
|
||||
replies: ReplyPayload[];
|
||||
chatId: string;
|
||||
@@ -146,17 +164,17 @@ export async function deliverReplies(params: {
|
||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
||||
}
|
||||
if (isGif) {
|
||||
await bot.api.sendAnimation(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendAnimation", runtime, () =>
|
||||
bot.api.sendAnimation(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
} else if (kind === "image") {
|
||||
await bot.api.sendPhoto(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendPhoto", runtime, () =>
|
||||
bot.api.sendPhoto(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
} else if (kind === "video") {
|
||||
await bot.api.sendVideo(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendVideo", runtime, () =>
|
||||
bot.api.sendVideo(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
} else if (kind === "audio") {
|
||||
const { useVoice } = resolveTelegramVoiceSend({
|
||||
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.
|
||||
await params.onVoiceRecording?.();
|
||||
try {
|
||||
await bot.api.sendVoice(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendVoice", runtime, () =>
|
||||
bot.api.sendVoice(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
} catch (voiceErr) {
|
||||
// Fall back to text if voice messages are forbidden in this chat.
|
||||
// This happens when the recipient has Telegram Premium privacy settings
|
||||
@@ -204,14 +222,14 @@ export async function deliverReplies(params: {
|
||||
}
|
||||
} else {
|
||||
// Audio file - displays with metadata (title, duration) - DEFAULT
|
||||
await bot.api.sendAudio(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendAudio", runtime, () =>
|
||||
bot.api.sendAudio(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await bot.api.sendDocument(chatId, file, {
|
||||
...mediaParams,
|
||||
});
|
||||
await withMediaErrorHandler("sendDocument", runtime, () =>
|
||||
bot.api.sendDocument(chatId, file, { ...mediaParams }),
|
||||
);
|
||||
}
|
||||
if (replyToId && !hasReplied) {
|
||||
hasReplied = true;
|
||||
|
||||
Reference in New Issue
Block a user