Recover cleanly from Telegram markdown parse errors
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user