Summary
@chat-adapter/telegram (v4.29.0) silently truncates an outbound message and produces an invalid MarkdownV2 payload when the text contains a bare URL whose URL contains an unescaped MarkdownV2 entity marker (_, *, ~, or an odd number of backticks). Telegram then rejects the send with:
Bad Request: can't parse entities: Can't find end of a URL at byte offset N
Because it's a deterministic ValidationError, retries don't help and the message never gets delivered.
Root cause
-
A bare URL such as https://example.com/org/org_abc123def/billing is turned into a link node by remark-gfm autolink-literal.
-
renderMarkdownV2 renders it as a self-link: [escaped-label](raw-url). The label is escaped via escapeMarkdownV2 (so its _ becomes \_), but the URL in the (...) part goes through escapeLinkUrl, whose regex only escapes ) and \:
var LINK_URL_SPECIAL_CHARS = /([)\\])/g;
So the _ inside org_abc123def stays unescaped.
-
That lone unescaped _ makes the whole message's count of unescaped _ odd.
-
postMessage calls truncateForTelegram, which for MarkdownV2 runs trimToMarkdownV2SafeBoundary even when the text is well under the 4096 limit. That function sees the odd _ as an unpaired italic marker and truncates the message at that position — cutting [label](https://example.com/org/org with no closing ).
-
Telegram receives a text-link whose URL never terminates → "Can't find end of a URL".
Minimal reproduction
import { TelegramFormatConverter } from '@chat-adapter/telegram';
const c = new TelegramFormatConverter();
const md = 'See billing: https://example.com/org/org_abc123def456ghi789jkl/billing';
console.log(JSON.stringify(c.fromMarkdown(md)));
// The (url) part contains an unescaped `_`. When postMessage runs
// truncateForTelegram -> trimToMarkdownV2SafeBoundary on this, the message
// is truncated at the `_`, leaving `[...](https://example.com/org/org` open,
// and Telegram sendMessage/editMessageText returns
// Bad Request: can't parse entities: Can't find end of a URL
Any URL containing _, *, or ~ triggers it. A URL with _ (very common in tenant/org/resource IDs) reproduces every time.
Suggested fix
Make escapeLinkUrl escape the MarkdownV2 entity markers inside URLs so the unescaped-marker counting in trimToMarkdownV2SafeBoundary stays balanced (Telegram unescapes \_/\*/\~/\` back to the literal inside the (...) URL part, so the URL is preserved):
-var LINK_URL_SPECIAL_CHARS = /([)\\])/g;
+var LINK_URL_SPECIAL_CHARS = /([)\\*_~`])/g;
Alternatively, trimToMarkdownV2SafeBoundary could ignore markers that fall inside a link's (...) URL span. Either way, the "safe boundary" trim should never be able to cut a message in the middle of a [...](...) link.
Environment
@chat-adapter/telegram@4.29.0, chat@4.29.0
- Node 20, parse mode MarkdownV2
Summary
@chat-adapter/telegram(v4.29.0) silently truncates an outbound message and produces an invalid MarkdownV2 payload when the text contains a bare URL whose URL contains an unescaped MarkdownV2 entity marker (_,*,~, or an odd number of backticks). Telegram then rejects the send with:Because it's a deterministic
ValidationError, retries don't help and the message never gets delivered.Root cause
A bare URL such as
https://example.com/org/org_abc123def/billingis turned into a link node byremark-gfmautolink-literal.renderMarkdownV2renders it as a self-link:[escaped-label](raw-url). The label is escaped viaescapeMarkdownV2(so its_becomes\_), but the URL in the(...)part goes throughescapeLinkUrl, whose regex only escapes)and\:So the
_insideorg_abc123defstays unescaped.That lone unescaped
_makes the whole message's count of unescaped_odd.postMessagecallstruncateForTelegram, which for MarkdownV2 runstrimToMarkdownV2SafeBoundaryeven when the text is well under the 4096 limit. That function sees the odd_as an unpaired italic marker and truncates the message at that position — cutting[label](https://example.com/org/orgwith no closing).Telegram receives a text-link whose URL never terminates → "Can't find end of a URL".
Minimal reproduction
Any URL containing
_,*, or~triggers it. A URL with_(very common in tenant/org/resource IDs) reproduces every time.Suggested fix
Make
escapeLinkUrlescape the MarkdownV2 entity markers inside URLs so the unescaped-marker counting intrimToMarkdownV2SafeBoundarystays balanced (Telegram unescapes\_/\*/\~/\`back to the literal inside the(...)URL part, so the URL is preserved):Alternatively,
trimToMarkdownV2SafeBoundarycould ignore markers that fall inside a link's(...)URL span. Either way, the "safe boundary" trim should never be able to cut a message in the middle of a[...](...)link.Environment
@chat-adapter/telegram@4.29.0,chat@4.29.0