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

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

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

richmenuswitch アクションの仕組み

LINE Messaging APIの richmenuswitch アクション は、ユーザーがリッチメニュー上のボタンをタップしたとき、表示中のメニューを別のメニューに切り替える機能です。LINE Messaging API の中でも、リッチメニューはユーザーとの対話インターフェースとして最も利用頻度の高い機能の一つです。

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

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

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

なお、1つのLINEチャネルで作成できるリッチメニューエイリアスは最大1,000個です。実運用では十分な数ですが、テスト環境で大量にエイリアスを作成している場合は上限に注意してください。

エイリアス更新の罠は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)に慣れた開発者には直感に反します。

サイレントに壊れる理由

このバグが発見しにくいのは、デプロイ処理自体が成功したように見えるためです。特にCI/CDパイプラインで自動デプロイを構築している場合、エイリアス更新の失敗がパイプライン全体の成否に影響しない設計になっていると、デプロイ成功の通知を受け取りながらメニュー切替だけが壊れるという状況が起こります。

典型的なフォールバックパターンを考えてみます。

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

サイレント障害を防ぐためのロギング

この種の問題を早期に検知するには、エイリアス操作のレスポンスを必ずログに残すことが重要です。LINE Messaging APIのエラーレスポンス仕様に基づき、ステータスコードとエラーメッセージの両方を記録します。

const res = await fetch(url, options);
const body = await res.text();

if (!res.ok) {
  console.error(JSON.stringify({
    operation: 'alias_update',
    aliasId,
    status: res.status,
    body,
    method: options.method,
    timestamp: new Date().toISOString(),
  }));
  // CI/CDパイプラインでは非ゼロ終了コードで失敗を伝播させる
  if (process.env.CI) {
    process.exit(1);
  }
}

特に 405 Method Not Allowed400 Bad Request(エイリアス重複)が連続して発生している場合、本記事で解説しているHTTPメソッドの誤りが原因である可能性が高いといえます。

確実な解決策としての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の不正、エイリアス数の上限超過など)が明確に得られます。

Delete-Createパターンのレート制限に関する注意

LINE Messaging APIにはレート制限が設定されています。Delete-Createパターンでは1回のエイリアス更新で2つのAPIリクエストを発行するため、複数のエイリアスを一括更新する場合はレート制限に配慮が必要です。

// 複数エイリアスの一括更新時はインターバルを入れる
async function batchUpdateAliases(
  token: string,
  updates: Array<{ aliasId: string; richMenuId: string }>
): Promise<void> {
  for (const { aliasId, richMenuId } of updates) {
    await updateRichMenuAlias(token, aliasId, richMenuId);
    // APIレート制限を考慮して100msのインターバルを設ける
    await new Promise(resolve => setTimeout(resolve, 100));
  }
}

A/B面のタブ切替で使用するエイリアスは通常2〜4個程度なのでレート制限に抵触する可能性は低いですが、テスト環境やステージング環境で多数のメニューバリエーションを管理している場合は注意してください。

LINE Bot SDKを使った安全なアプローチ

HTTPメソッドの選択ミスを根本的に防ぐもう一つの方法は、公式のLINE Bot SDK for Node.js を使うことです。SDKはHTTPリクエストの詳細を抽象化しているため、メソッドの選択ミスが発生しません。

@line/bot-sdk v8以降では、APIクライアントが機能別に分離され、messagingApi.MessagingApiClient クラスを使用します。v7以前の Client クラスからの移行が必要な場合は、公式の移行ガイドを参照してください。

import { messagingApi } from '@line/bot-sdk';

const client = new messagingApi.MessagingApiClient({
  channelAccessToken: token,
});

// SDKが正しいHTTPメソッド(POST)を内部で使用する
await client.updateRichMenuAlias(aliasId, {
  richMenuAliasId: aliasId,
  richMenuId: newMenuId,
});

SDKを使うメリットは、HTTPメソッドの正誤を気にする必要がなくなるだけでなく、TypeScriptの型定義によって必須パラメータの漏れもコンパイル時に検出できる点です。ただし、Delete-Createパターンの冪等性というメリットはSDK経由でも自動的には得られないため、リトライ戦略が必要な場合は前述のパターンと組み合わせて使うとよいでしょう。

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

リッチメニュー画像変更時のデプロイフローを全体的に整理します。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の旧メニュー削除より前に来ていることも重要です。逆にすると、エイリアスが一時的に削除済みメニューを指す瞬間が生まれ、その間にユーザーがタブ切替を行うとエラーになります。

なお、リッチメニュー画像の仕様として、画像サイズは2500x1686px または 2500x843px、ファイル形式はJPEGまたはPNG、ファイルサイズは1MB以内である必要があります。画像アップロード(ステップ2)が失敗する場合は、まずこれらの仕様を確認してください。

このデプロイフローをシェルスクリプトやCI/CDパイプラインに組み込む場合は、各ステップのレスポンスステータスを検証し、失敗時に後続ステップを実行しないよう制御してください。特にステップ3(エイリアス更新)が失敗した状態でステップ4(旧メニュー削除)を実行すると、エイリアスが削除済みメニューを指す最悪のケースに陥ります。

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

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

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

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

    if (!res.ok) {
      errors.push(`エイリアス ${aliasId} の取得失敗: HTTP ${res.status}`);
      continue;
    }

    const data = await res.json();

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

  if (errors.length > 0) {
    throw new Error(`エイリアス検証エラー:\n${errors.join('\n')}`);
  }
}

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

トラブルシューティングチェックリスト

リッチメニューのタブ切替が動かなくなった場合、以下の手順で原因を切り分けます。

  1. エイリアスの現在値を確認するGET /v2/bot/richmenu/alias/{aliasId} で、エイリアスが指しているリッチメニューIDを取得します。
  2. そのリッチメニューが存在するか確認するGET /v2/bot/richmenu/{richMenuId} で、エイリアスが指すメニューが実際に存在するかを確認します。404が返る場合、エイリアスが削除済みメニューを指しています。
  3. デプロイログを確認する — エイリアス更新のHTTPレスポンスコードを確認します。405 が記録されていれば、本記事で解説したHTTPメソッドの問題です。429 Too Many Requests が記録されている場合はレート制限に抵触しているため、リクエスト間隔を調整してください。
  4. エイリアスを再設定する — Delete-Createパターンでエイリアスを正しいメニューIDに向け直します。
  5. richmenuswitch アクションの richMenuAliasId を確認する — メニュー定義のJSON内でエイリアスIDのスペルミスがないか確認します。
  6. Webhookの応答を確認する — richmenuswitch アクションはpostbackイベントとしてWebhookに通知されます。Webhookの応答が遅い場合(タイムアウト)、LINE側で切替処理が中断される可能性があります。

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

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

特にLINE Messaging APIでは、エイリアスの作成も更新も同じPOSTメソッドで設計されています。これはREST APIの設計としては珍しいパターンですが、エンドポイントのパス構造(作成はコレクション /alias へのPOST、更新は個別リソース /alias/{id} へのPOST)で操作を区別する設計です。同様のパターンはGoogle Calendar APIなど他のサービスでも見られ、REST APIの設計が必ずしもCRUDとHTTPメソッドの1対1対応にならない実例として理解しておくと、他のAPIの統合時にも役立ちます。REST APIの基本的な設計原則については、REST APIの基本と活用例も参照してください。

外部APIを統合する際は、RESTfulの慣習を前提とせず、公式リファレンスでHTTPメソッドを一つずつ確認する習慣を持つことが、この種のサイレントな障害を未然に防ぐ最も確実な方法です。LINE公式アカウントの開発全般については、LIFF連絡フォームの実装ガイドでも関連する実装パターンを解説しています。