Telegramの書式エラー対策:動的テキストをHTMLで安全に送る
固定文なら送れるTelegramメッセージが、顧客名やAIの回答を挿入すると失敗する場合は、再試行の前に書式モードを確認してください。プレーンテキストなら parse_mode を省略し、原文を送ります。HTMLなら動的な文字列をエスケープしてから、アプリが管理するテンプレートに挿入します。正しいJSONでも、Telegramの書式規則に適合するとは限りません。
要点
- プレーンテキスト、HTML、MarkdownV2は別々のレンダリング方式です。名前だけ変更しても解決しません。
- 原文と整形後の文字列を分け、出力時に一度だけエスケープします。
- 確認済みの書式エラー、レート制限、結果不明の送信を区別します。
- UnifyPortでは、Bot APIのトップレベル項目ではなく
provider_data.parse_modeを使います。
まず書式の問題を切り分ける
Telegram Bot APIリファレンスでは、parse_mode をテキストのエンティティ解析に使うと説明しています。HTMLモードで使えるのは指定されたタグだけで、ブラウザーのHTML表示とは異なります。また、従来のMarkdownとMarkdownV2も別のモードです。
管理下のチャットで、機密情報を含まない短い文章から確認します。
- SDKや自動化ツールが最終的に生成したリクエストを確認します。既定値によって書式モードが追加される場合があります。
- 書式オプションなしで短い原文を送ります。HTMLやMarkdown向けに加工済みの文字列は使いません。
- 固定文字だけの小さなHTMLテンプレートを追加します。
- 動的な値をHTMLのテキストとしてエスケープしてから挿入します。
- APIの結果と、クライアントでの実際の表示をそれぞれ確認します。
プレーンテキストも失敗するなら、実際のエラーに従い宛先、認証、長さを調べます。webhookの応答に送信処理を埋め込んでいる場合は、独立したsendMessageリクエストとの比較を参照してください。埋め込み方式では送信結果を取得できません。
動的な値をエスケープし、任意のタグを受け入れない
TelegramのHTMLモードでは、タグやエンティティの一部ではない <、>、& を対応するHTMLエンティティに置き換えます。次のJavaScript例は、タグの間に挿入するテキストだけを処理します。汎用HTMLサニタイザー、Markdown変換器、URL属性の検証器ではありません。
function escapeTelegramHtmlText(value) {
if (typeof value !== 'string') {
throw new TypeError('Expected text');
}
return value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>');
}
function renderSupportAnswer(customerName, answer) {
return `<b>${escapeTelegramHtmlText(customerName)}</b>\n` +
escapeTelegramHtmlText(answer);
}
const rendered = renderSupportAnswer(
'R&D <Team>',
'Use x < 5 & keep the original value.'
);
& を最初に置き換えるのは、その後の処理が作るエンティティを再度エスケープしないためです。生成されるソースは次の形です。
<b>R&D <Team></b>
Use x < 5 & keep the original value.
受信者には元の記号が表示され、名前だけが太字になる想定です。タグ構造はアプリ側で管理します。顧客名やAI回答に含まれるタグは、書式指示ではなく内容として扱います。
組み立て済みのテンプレート全体をエスケープすると、意図したタグまで文字として表示されます。一方、後で JSON.stringify を呼ぶからといってHTMLのエスケープを省略するのも誤りです。JSONのシリアライズとTelegramの書式解析は別の処理です。
リンクを生成する場合は、宛先URLの検証と属性に適した処理を備えた専用ビルダーを用意してください。この関数は動的属性を扱いません。書式を増やす場合も、公式の対応タグに沿ってレンダラーを拡張・テストします。
APIごとの正しい場所に設定する
| 接続方式 | テキストの項目 | 書式オプション |
|---|---|---|
Telegram公式Bot APIの sendMessage | text | parse_mode |
| UnifyPortの統一テキスト送信 | message.text | provider_data.parse_mode |
UnifyPortは非公式インターフェースで接続済みメッセージングアカウントを扱い、Bot APIとは異なるリクエスト形式を使います。テキスト送信リファレンスには POST /v1/messages、空でない message.text、プロバイダー固有の parse_mode が定義されています。送信機能マトリクスでは、この拡張はTelegram向けです。全チャネルのHTML対応を意味しません。
以下は先ほどのレンダラーを使うリクエストビルダーです。実際のアカウントIDと、選択した会話のID・種類を渡します。JSONを作るだけで、送信や再試行は実行しません。実機テスト済みの完成アプリという意味でもありません。
function buildTelegramReply({ accountId, conversation, name, answer }) {
return {
account_id: accountId,
to: { id: conversation.id, type: conversation.type },
message: {
type: 'text',
text: renderSupportAnswer(name, answer)
},
provider_data: { parse_mode: 'HTML' }
};
}
文書化されたエンドポイントへ送信し、サーバー側の X-Api-Key で認証します。プレーンテキストでは provider_data.parse_mode を省略し、HTML加工済み文字列ではなく元の回答を使います。対応が文書化されていないBot APIの chat_id や entities を、そのまま統一リクエストに追加しないでください。
LINEとTelegramを同じワーカーで扱う場合も、レンダラーはチャネル別に選びます。ルーティングの共通化と書式の共通化は別です。
再試行より先に内容の境界をテストする
実際の顧客会話ではなく、合成したテストデータを使います。
| 入力例 | 確認すること |
|---|---|
& と山括弧を含む名前 | 元の記号が表示され、管理下のタグだけが書式になる |
原文に含まれる文字列 & | 勝手にデコードせず原文として表示する |
| 絵文字、多言語、改行 | シリアライズと表示で内容を壊さない |
| 空の入力や長い生成回答 | 送信前に入力検証と長さの方針を適用する |
| 同じ原文の二度のレンダリング | 毎回原文から開始し、出力を再加工しない |
長文は原文を意味のある単位で分割し、各部分を個別にレンダリングします。生成済みHTMLを任意の位置で切ると、タグやエンティティを分断する可能性があります。選択したメソッドの現在の長さ制限を確認してください。この例は長さ検証やリッチメッセージを実装していません。
確定した書式エラーは内容を修正すべき問題で、待機して同じ文を送り直す問題ではありません。レート制限はTelegram 429と送信キューのガイドに従います。応答を得られないタイムアウトでは結果不明として保持し、プレーンテキスト版を自動送信しないでください。すでに届いた返信を重複させる可能性があります。
よくある質問
JSONが正しければ書式も正しいですか?
いいえ。JSON解析に成功しても、その中の文字列が書式規則に違反することがあります。
エラーになったらすべてMarkdownV2に変えるべきですか?
いいえ。MarkdownV2には独自のエスケープ規則があります。モードを決め、それに合うレンダラーを使います。
プレーンテキストでHTMLエンティティが見えるのはなぜですか?
解析モードを外した後もHTML加工済み文字列を使っていないか確認してください。プレーンテキストは原文から生成します。
HTML文書全体をこの関数に渡せますか?
この用途には適しません。管理下のタグ間に置く動的テキスト専用で、任意のタグ、属性、リンク、長さを検証しません。
次のステップと出典
プレーンテキストとHTMLのレンダラーを分け、管理下のTelegram会話で確認します。接続済みアカウントならUnifyPortのテキスト送信仕様を基準にしてください。
確認日:2026-10-21。
メッセージ連携を安定したプロダクトパイプラインへ。
まずは 1 つの API で送信を始め、標準イベントですべての inbound メッセージを業務システムへ戻しましょう。