Recover cleanly from Telegram markdown parse errors

This commit is contained in:
William Valentin
2026-02-22 21:18:19 -08:00
parent 07f4f99187
commit 89246e7da0
2 changed files with 55 additions and 8 deletions
+35
View File
@@ -161,6 +161,41 @@ describe('TelegramAdapter', () => {
}
});
it('falls back to plain text when Telegram rejects Markdown entities via description', async () => {
await adapter.connect();
mockSendMessage
.mockRejectedValueOnce({
error_code: 400,
description: "Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 43",
})
.mockResolvedValueOnce(undefined);
await adapter.send('100', { text: 'Status: **blocked**\nEvidence: `bad markdown' });
expect(mockSendMessage).toHaveBeenCalledTimes(2);
expect(mockSendMessage.mock.calls[0]).toEqual([100, 'Status: **blocked**\nEvidence: `bad markdown', { parse_mode: 'Markdown' }]);
expect(mockSendMessage.mock.calls[1]).toEqual([100, 'Status: **blocked**\nEvidence: `bad markdown']);
expect(adapter.status).toBe('connected');
expect(adapter.lastError).toBeUndefined();
});
it('falls back to plain text when parse-entity failure appears only in error message', async () => {
await adapter.connect();
mockSendMessage
.mockRejectedValueOnce(new Error(
"Call to 'sendMessage' failed! (400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 43)",
))
.mockResolvedValueOnce(undefined);
await adapter.send('100', { text: 'Status: **blocked**\nEvidence: `bad markdown' });
expect(mockSendMessage).toHaveBeenCalledTimes(2);
expect(mockSendMessage.mock.calls[0]).toEqual([100, 'Status: **blocked**\nEvidence: `bad markdown', { parse_mode: 'Markdown' }]);
expect(mockSendMessage.mock.calls[1]).toEqual([100, 'Status: **blocked**\nEvidence: `bad markdown']);
expect(adapter.status).toBe('connected');
expect(adapter.lastError).toBeUndefined();
});
// ── onMessage / inbound handling ──────────────────────────────
it('onMessage registers a handler that receives text messages', async () => {
+20 -8
View File
@@ -544,12 +544,9 @@ export class TelegramAdapter implements ChannelAdapter {
`sendMessage(markdown) chat=${chatId}`,
);
} catch (error) {
const description = error && typeof error === 'object' && 'description' in error
? String((error as { description?: unknown }).description)
: '';
const isParseError = description.includes("can't parse entities")
|| description.includes('message text is empty');
const parsed = this.parseTelegramError(error);
const isParseError = parsed.parseEntityError
|| parsed.message.toLowerCase().includes('message text is empty');
if (!isParseError) {
throw error;
@@ -677,6 +674,7 @@ export class TelegramAdapter implements ChannelAdapter {
retryAfterSec?: number;
message: string;
transient: boolean;
parseEntityError: boolean;
} {
const code = typeof error === 'object' && error !== null && 'error_code' in error
? Number((error as { error_code?: unknown }).error_code)
@@ -692,6 +690,7 @@ export class TelegramAdapter implements ChannelAdapter {
? error.message
: description ?? String(error);
const normalized = message.toLowerCase();
const normalizedDescription = description?.toLowerCase() ?? '';
const transient = code === 429
|| (typeof code === 'number' && code >= 500)
|| normalized.includes('timeout')
@@ -700,8 +699,19 @@ export class TelegramAdapter implements ChannelAdapter {
|| normalized.includes('econnreset')
|| normalized.includes('enotfound')
|| normalized.includes('network');
const parseEntityError = normalized.includes("can't parse entities")
|| normalized.includes('cant parse entities')
|| normalizedDescription.includes("can't parse entities")
|| normalizedDescription.includes('cant parse entities');
return { code, description, retryAfterSec, message, transient };
return {
code,
description,
retryAfterSec,
message,
transient,
parseEntityError,
};
}
private async sendWithRetry<T>(fn: () => Promise<T>, context: string): Promise<T> {
@@ -715,7 +725,9 @@ export class TelegramAdapter implements ChannelAdapter {
} catch (error) {
const parsed = this.parseTelegramError(error);
if (!parsed.transient || attempt >= maxAttempts) {
this.recordAdapterError(`Telegram API failure (${context}): ${parsed.message}`);
if (!parsed.parseEntityError) {
this.recordAdapterError(`Telegram API failure (${context}): ${parsed.message}`);
}
throw error;
}
const baseDelay = parsed.retryAfterSec