リッチメニューのタブ切替が突然動かなくなる

LINE公式アカウントの運用で、リッチメニューのA/B面切替(タブ切替)は標準的なUXパターンとして広く使われています。この切替は richmenuswitch アクションとリッチメニューエイリアスの組み合わせで実現しますが、管理画面から画像を更新した直後に切替ボタンだけが反応しなくなるという問題に遭遇することがあります。

他のボタン(URI遷移やメッセージ送信)は正常に動作しているのに、面切替だけが無反応になる。LINE Developers Consoleでアクション設定を確認しても問題は見当たらない。この記事では、その原因がLINE Messaging APIのエイリアス更新エンドポイントのHTTPメソッド仕様にあることを明らかにし、再発防止策を解説します。

richmenuswitch アクションの仕組み

LINE Messaging APIの richmenuswitch アクション は、ユーザーがリッチメニュー上のボタンをタップしたとき、表示中のメニューを別のメニューに切り替える機能です。

{
  "type": "richmenuswitch",
  "richMenuAliasId": "pre_b",
  "data": "switch-pre_b",
  "label": "ご利用案内"
}

ここで重要なのは、切替先を リッチメニューID(richmenu-xxxx)で直接指定するのではなく、エイリアスID(pre_bなど)を介して間接参照する設計になっている点です。エイリアスは名前付きポインタのような存在であり、実際のリッチメニューIDとの対応関係を Rich menu alias API で管理します。

この間接参照の設計には明確な利点があります。メニュー画像を更新するとき、LINEの仕様上、既存のリッチメニューの画像を差し替えることはできません。新しいリッチメニューを作成し、画像をアップロードし、古いメニューを削除する必要があります。このとき新しいメニューには新しいIDが割り当てられますが、エイリアスを更新すれば、他のメニューの richmenuswitch アクション定義を変更する必要がないのです。

エイリアス更新の罠はHTTPメソッドにある

リッチメニューのデプロイフローでは、新しいメニュー作成後にエイリアスを更新して新しいIDに向け直す処理が必要です。多くの開発者がRESTfulな慣習から、既存リソースの更新には PUT メソッドを使うコードを書きます。

// 一見正しそうだが、実は動かないコード
const res = await fetch(
  `https://api.line.me/v2/bot/richmenu/alias/${aliasId}`,
  {
    method: 'PUT',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ richMenuAliasId: aliasId, richMenuId: newMenuId }),
  }
);

このリクエストに対してLINE APIは 405 Method Not Allowed を返します。LINE Messaging APIの エイリアス更新エンドポイント が受け付けるメソッドは PUT ではなく POST です。

操作 HTTPメソッド エンドポイント
エイリアス作成 POST POST /v2/bot/richmenu/alias
エイリアス更新 POST POST /v2/bot/richmenu/alias/{richMenuAliasId}
エイリアス取得 GET GET /v2/bot/richmenu/alias/{richMenuAliasId}
エイリアス削除 DELETE DELETE /v2/bot/richmenu/alias/{richMenuAliasId}
エイリアス一覧 GET GET /v2/bot/richmenu/alias/list

作成と更新が同じ POST メソッドで設計されている点は、RFC 9110のPOSTの定義 に照らせばセマンティクス上の問題はないものの、RESTful設計の慣習(作成=POST、更新=PUT)に慣れた開発者には直感に反します。

サイレントに壊れる理由

このバグが発見しにくいのは、デプロイ処理自体が成功したように見えるためです。典型的なフォールバックパターンを考えてみます。

// PUTで更新を試みる → 失敗
const updateRes = await fetch(`/v2/bot/richmenu/alias/${aliasId}`, {
  method: 'PUT', ...
});

if (!updateRes.ok) {
  // エイリアスが存在しないと判断して新規作成を試みる
  await fetch('/v2/bot/richmenu/alias', {
    method: 'POST', ...
    body: JSON.stringify({ richMenuAliasId: aliasId, richMenuId: newId }),
  });
}

PUTが405で失敗し、フォールバック先の新規作成POSTも「エイリアスが既に存在する」エラーで失敗します。結果としてエイリアスは古いリッチメニューIDを指し続け、richmenuswitch ボタンは存在しないメニューを参照して無反応になります。

さらに厄介なのは、URI遷移やメッセージ送信など他のアクションタイプには影響がない点です。ユーザーは「切替ボタン以外は全部動いているのに」という状況に直面し、原因の切り分けに時間を要します。

確実な解決策としてのDelete-Createパターン

最も堅牢な解決策は、エイリアスの更新を削除→再作成の2ステップで実装することです。

async function updateRichMenuAlias(
  token: string,
  aliasId: string,
  richMenuId: string
): Promise<void> {
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  };

  // Step 1: 既存エイリアスを削除(存在しなくてもエラーにしない)
  await fetch(
    `https://api.line.me/v2/bot/richmenu/alias/${aliasId}`,
    { method: 'DELETE', headers }
  );

  // Step 2: 新しいIDでエイリアスを再作成
  const createRes = await fetch(
    'https://api.line.me/v2/bot/richmenu/alias',
    {
      method: 'POST',
      headers,
      body: JSON.stringify({ richMenuAliasId: aliasId, richMenuId }),
    }
  );

  if (!createRes.ok) {
    const err = await createRes.text();
    throw new Error(`エイリアス作成失敗: ${err}`);
  }
}

このパターンが優れている理由は3つあります。

HTTPメソッドの曖昧さを回避できます。 DELETEとPOSTはどちらもLINE APIで明確にサポートされているメソッドであり、PUTかPOSTかという混乱を根本的に排除できます。

冪等性を自然に担保できます。 何らかの理由でデプロイが中断され再実行された場合でも、削除→作成の流れは常に同じ結果を生みます。エイリアスが存在しない状態でDELETEを呼んでもエラーにはならないためです。

デバッグが容易になります。 作成ステップは明示的にPOSTで行うため、失敗した場合のエラーメッセージ(IDの不正、エイリアス数の上限超過など)が明確に得られます。

デプロイフロー全体の設計

リッチメニュー画像変更時のデプロイフローを全体的に整理します。LINE公式のリッチメニュー管理ドキュメント に基づく各ステップの正確なAPIエンドポイントは以下の通りです。

1. POST /v2/bot/richmenu             → 新しいリッチメニュー作成
2. POST /v2/bot/richmenu/{id}/content → 画像アップロード
3. DELETE + POST /v2/bot/richmenu/alias → エイリアス更新
4. DELETE /v2/bot/richmenu/{oldId}    → 古いメニュー削除
5. POST /v2/bot/user/all/richmenu/{id} → デフォルトメニュー設定(必要な場合)

ステップ3のエイリアス更新がステップ4の旧メニュー削除より前に来ていることも重要です。逆にすると、エイリアスが一時的に削除済みメニューを指す瞬間が生まれ、その間にユーザーがタブ切替を行うとエラーになります。

エイリアスの状態を検証する

デプロイ後、エイリアスが正しく設定されているかを検証する仕組みをデプロイフローに組み込んでおくと、問題の早期発見に役立ちます。

async function verifyAliases(token: string, expected: Record<string, string>) {
  const headers = { 'Authorization': `Bearer ${token}` };

  for (const [aliasId, expectedMenuId] of Object.entries(expected)) {
    const res = await fetch(
      `https://api.line.me/v2/bot/richmenu/alias/${aliasId}`,
      { headers }
    );
    const data = await res.json();

    if (data.richMenuId !== expectedMenuId) {
      console.error(
        `エイリアス ${aliasId} の不一致: ` +
        `期待=${expectedMenuId}, 実際=${data.richMenuId}`
      );
    }
  }
}

エイリアス一覧API を使えば、チャネルに登録されている全エイリアスを一括で確認することもできます。管理画面にエイリアス検証ボタンを設けておくと、運用時のトラブルシューティングが格段に楽になります。

APIドキュメントの読み方で防げる問題

この問題は、LINE Messaging APIの リッチメニューエイリアスのリファレンス を正確に読めば防げるものです。しかし現実には、MDN Web DocsのHTTPメソッド解説 にもあるように、PUTが「リソースの更新」に使われるという一般的な理解があるため、APIリファレンスを流し読みすると見落としがちです。

特にLINE Messaging APIでは、エイリアスの作成も更新も同じPOSTメソッドで設計されています。これはREST APIの設計としては珍しいパターンですが、エンドポイントのパス構造(作成はコレクション /alias へのPOST、更新は個別リソース /alias/{id} へのPOST)で操作を区別する設計です。

外部APIを統合する際は、RESTfulの慣習を前提とせず、公式リファレンスでHTTPメソッドを一つずつ確認する習慣を持つことが、この種のサイレントな障害を未然に防ぐ最も確実な方法です。