[ #LINEDC ] HonoでLINE Message API用の Webhook APIを作る
この記事では、HonoフレームワークでLINE Message APIのWebhook APIを作成する方法を解説しています。署名検証の実装方法や、メッセージ処理関数の分離、リクエストボディの取り扱いなどのポイントが紹介されています。LINEボットを構築する際の参考になる実践的な内容です。
目次
LINE上でChatbotを作るには、LINEが受信したメッセージを転送するためのWebhook APIが必要です。一般的には、POSTリクエストを受け付けるパブリックなAPIを作ることになります。今回はHonoでAPIを提供する際の署名検証処理系をまとめてみました。
最終コード
コードがあればよい。という方向けに、全体像です。TypeScriptですので、JavaScriptで実装される方は型情報を取り除いてお使いください。
import { Hono } from 'hono';
import { validateSignature, WebhookEvent } from '@line/bot-sdk';
interface LineWebhookRequest {
  destination: string;
  events: WebhookEvent[];
}
type Env = {
  LINE_CHANNEL_SECRET: string;
};
const app = new Hono<{ Bindings: Env }>();
// LINE Webhook処理
app.post('/line', async (c) => {
  try {
    const signature = c.req.header('X-Line-Signature');
    if (!signature) {
      return c.text('Signature required', 401);
    }
    // リクエストボディを検証
    const body = await c.req.text();
    const isValid = validateSignature(body, c.env.LINE_CHANNEL_SECRET, signature);
    
    if (!isValid) {
      return c.text('Invalid signature', 401);
    }
    // Webhookイベントを処理
    const webhookRequest = JSON.parse(body) as LineWebhookRequest;
    const events = webhookRequest.events;
    console.log(`LINEイベント受信: ${events.length}件`);
    // イベントごとに処理
    for (const event of events) {
      // @TODO メッセージごとの処理を書く
    }
    return c.text('OK');
  } catch (error) {
    console.error('LINE Webhookエラー:', error);
    return c.text('Internal Server Error', 500);
  }
});
Webhookを実装する際のポイント
検証系やメッセージ処理系を作るにあたって、意識しておくと良いかなと思った部分を2つ紹介します。
ポイント1: メッセージ処理関数は、別で用意する
LINEのWebhookは、1通ごとにイベントが届くわけではない様子です。というのも、Webhookで送信されるデータを見ると、イベントデータが配列型式になっています。そのため、実際の処理系統は、下のコードのようにループの中で実行することになります。
    for (const event of events) {
      // @TODO メッセージごとの処理を書く
    }
HonoでAPIを登録する部分( app.post() )には、署名検証系のみ実装しておきましょう。これによってリクエストの検証とメッセージ処理の責務が分担できますので、コードの見通しもよくなります。
ポイント2: リクエストBodyは一旦テキストにする
JSONでデータが飛んでくるため、このタイプのAPIを作る時はawait c.req.json()でリクエスト内容を取得したくなります。ですがStripeと同様に、LINEでもWebhookの署名検証処理を動かすステップではテキストデータが必要となります。そのためリクエスト内容は、await c.req.text()で取得するようにしましょう。
    // リクエストボディを検証
    const body = await c.req.text();
    const isValid = validateSignature(body, c.env.LINE_CHANNEL_SECRET, signature);
    if (!isValid) {
      return c.text('Invalid signature', 401);
    }
検証が終わった後は、JSON.parseでテキストからオブジェクトに変換してやります。
    const webhookRequest = JSON.parse(body) as LineWebhookRequest;
    const events = webhookRequest.events;
二度手間に見えますが、セキュリティ対策としても重要ですので、意識しておくと良さそうです。