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 ──────────────────────────────
|
// ── onMessage / inbound handling ──────────────────────────────
|
||||||
|
|
||||||
it('onMessage registers a handler that receives text messages', async () => {
|
it('onMessage registers a handler that receives text messages', async () => {
|
||||||
|
|||||||
@@ -544,12 +544,9 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
`sendMessage(markdown) chat=${chatId}`,
|
`sendMessage(markdown) chat=${chatId}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const description = error && typeof error === 'object' && 'description' in error
|
const parsed = this.parseTelegramError(error);
|
||||||
? String((error as { description?: unknown }).description)
|
const isParseError = parsed.parseEntityError
|
||||||
: '';
|
|| parsed.message.toLowerCase().includes('message text is empty');
|
||||||
|
|
||||||
const isParseError = description.includes("can't parse entities")
|
|
||||||
|| description.includes('message text is empty');
|
|
||||||
|
|
||||||
if (!isParseError) {
|
if (!isParseError) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -677,6 +674,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
retryAfterSec?: number;
|
retryAfterSec?: number;
|
||||||
message: string;
|
message: string;
|
||||||
transient: boolean;
|
transient: boolean;
|
||||||
|
parseEntityError: boolean;
|
||||||
} {
|
} {
|
||||||
const code = typeof error === 'object' && error !== null && 'error_code' in error
|
const code = typeof error === 'object' && error !== null && 'error_code' in error
|
||||||
? Number((error as { error_code?: unknown }).error_code)
|
? Number((error as { error_code?: unknown }).error_code)
|
||||||
@@ -692,6 +690,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
? error.message
|
? error.message
|
||||||
: description ?? String(error);
|
: description ?? String(error);
|
||||||
const normalized = message.toLowerCase();
|
const normalized = message.toLowerCase();
|
||||||
|
const normalizedDescription = description?.toLowerCase() ?? '';
|
||||||
const transient = code === 429
|
const transient = code === 429
|
||||||
|| (typeof code === 'number' && code >= 500)
|
|| (typeof code === 'number' && code >= 500)
|
||||||
|| normalized.includes('timeout')
|
|| normalized.includes('timeout')
|
||||||
@@ -700,8 +699,19 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
|| normalized.includes('econnreset')
|
|| normalized.includes('econnreset')
|
||||||
|| normalized.includes('enotfound')
|
|| normalized.includes('enotfound')
|
||||||
|| normalized.includes('network');
|
|| 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> {
|
private async sendWithRetry<T>(fn: () => Promise<T>, context: string): Promise<T> {
|
||||||
@@ -715,7 +725,9 @@ export class TelegramAdapter implements ChannelAdapter {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const parsed = this.parseTelegramError(error);
|
const parsed = this.parseTelegramError(error);
|
||||||
if (!parsed.transient || attempt >= maxAttempts) {
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
const baseDelay = parsed.retryAfterSec
|
const baseDelay = parsed.retryAfterSec
|
||||||
|
|||||||
Reference in New Issue
Block a user