← 全記事
チュートリアル

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も別のモードです。

管理下のチャットで、機密情報を含まない短い文章から確認します。

  1. SDKや自動化ツールが最終的に生成したリクエストを確認します。既定値によって書式モードが追加される場合があります。
  2. 書式オプションなしで短い原文を送ります。HTMLやMarkdown向けに加工済みの文字列は使いません。
  3. 固定文字だけの小さなHTMLテンプレートを追加します。
  4. 動的な値をHTMLのテキストとしてエスケープしてから挿入します。
  5. 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('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;');
}

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&amp;D &lt;Team&gt;</b>
Use x &lt; 5 &amp; keep the original value.

受信者には元の記号が表示され、名前だけが太字になる想定です。タグ構造はアプリ側で管理します。顧客名やAI回答に含まれるタグは、書式指示ではなく内容として扱います。

組み立て済みのテンプレート全体をエスケープすると、意図したタグまで文字として表示されます。一方、後で JSON.stringify を呼ぶからといってHTMLのエスケープを省略するのも誤りです。JSONのシリアライズとTelegramの書式解析は別の処理です。

リンクを生成する場合は、宛先URLの検証と属性に適した処理を備えた専用ビルダーを用意してください。この関数は動的属性を扱いません。書式を増やす場合も、公式の対応タグに沿ってレンダラーを拡張・テストします。

APIごとの正しい場所に設定する

接続方式テキストの項目書式オプション
Telegram公式Bot APIの sendMessagetextparse_mode
UnifyPortの統一テキスト送信message.textprovider_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を同じワーカーで扱う場合も、レンダラーはチャネル別に選びます。ルーティングの共通化と書式の共通化は別です。

再試行より先に内容の境界をテストする

実際の顧客会話ではなく、合成したテストデータを使います。

入力例確認すること
& と山括弧を含む名前元の記号が表示され、管理下のタグだけが書式になる
原文に含まれる文字列 &amp;勝手にデコードせず原文として表示する
絵文字、多言語、改行シリアライズと表示で内容を壊さない
空の入力や長い生成回答送信前に入力検証と長さの方針を適用する
同じ原文の二度のレンダリング毎回原文から開始し、出力を再加工しない

長文は原文を意味のある単位で分割し、各部分を個別にレンダリングします。生成済みHTMLを任意の位置で切ると、タグやエンティティを分断する可能性があります。選択したメソッドの現在の長さ制限を確認してください。この例は長さ検証やリッチメッセージを実装していません。

確定した書式エラーは内容を修正すべき問題で、待機して同じ文を送り直す問題ではありません。レート制限はTelegram 429と送信キューのガイドに従います。応答を得られないタイムアウトでは結果不明として保持し、プレーンテキスト版を自動送信しないでください。すでに届いた返信を重複させる可能性があります。

よくある質問

JSONが正しければ書式も正しいですか?

いいえ。JSON解析に成功しても、その中の文字列が書式規則に違反することがあります。

エラーになったらすべてMarkdownV2に変えるべきですか?

いいえ。MarkdownV2には独自のエスケープ規則があります。モードを決め、それに合うレンダラーを使います。

プレーンテキストでHTMLエンティティが見えるのはなぜですか?

解析モードを外した後もHTML加工済み文字列を使っていないか確認してください。プレーンテキストは原文から生成します。

HTML文書全体をこの関数に渡せますか?

この用途には適しません。管理下のタグ間に置く動的テキスト専用で、任意のタグ、属性、リンク、長さを検証しません。

次のステップと出典

プレーンテキストとHTMLのレンダラーを分け、管理下のTelegram会話で確認します。接続済みアカウントならUnifyPortのテキスト送信仕様を基準にしてください。

確認日:2026-10-21。

UnifyPort API

メッセージ連携を安定したプロダクトパイプラインへ。

まずは 1 つの API で送信を始め、標準イベントですべての inbound メッセージを業務システムへ戻しましょう。