一度で「検証」を通る Webhook URL。
コンソールは https の URL しか受け付けず、「検証」(Verify)ボタンは 200 を求めます。「Webhookの再送」は設定を見つけて有効にしない限りオフのままで、LINE はリトライの頻度を一切明かしません。AnyHook は「検証」に応答し、すべてのイベントをエッジで保存し、確認できるスケジュールでハンドラーへのリトライを続けます。
タイミングの問題
LINE Developers コンソールは、https 以外の Webhook URL を保存しません。その横にある「検証」ボタンは実際に POST を送り、200 が返ってこなければなりません。トンネルはホスト名が変わるまでは検証を通りますが、変わった途端にボットはどこも指していない状態になり、コンソールはそれを知らせてくれません。もっと深刻なのは、受信し損ねたときに何が起きるかです。「Webhookの再送」は初期状態で無効です。有効にしても、リトライの回数と間隔は非公開で予告なく変わることがあると LINE は明言しています。
返信側にも独自の期限があります。応答トークンの有効期間は 1 分で、使えるのは 1 回だけです。そのため、クラッシュやデプロイ、リトライの後で遅れて受け取ったイベントには、無料の応答メッセージではもう返信できません。ボットサーバーに長く到達できない状態が続くと、LINE はそのサーバーへの Webhook 送信を完全に止めることがあり、そのしきい値も公開されていません。
コンソールで AnyHook を指定し、ヘッダーを 1 つ検証する
App(Webhook の送信元ごとに作る受信設定)で送信元に LINE を選び、チャネルシークレットを貼り付けます。AnyHook は x-line-signature(生のリクエストボディの HMAC-SHA256 を base64 にしたもの)をエッジで検証するので、偽造リクエストがサーバーに届くことも、クォータを消費することもありません。すべての配信は新しい AnyHook-Signature で再署名されます。
// 1. In AnyHook: set the app's source to LINE, paste your Channel Secret.
//
// 2. In LINE Developers Console -> Messaging API:
// Webhook URL: https://in.anyhook.net/{user-slug}/{app-slug}
// Press Verify (AnyHook answers 200), enable "Use webhook",
// and turn ON "Webhook redelivery" while you are there.
import { verifyWebhook } from "anyhook-verify";
export async function POST(req: Request) {
const ok = await verifyWebhook(req, process.env.ANYHOOK_SIGNING_SECRET!);
if (!ok) return new Response("invalid signature", { status: 401 });
const event = await req.json();
for (const e of event.events ?? []) {
if (e.type !== "message") continue;
const answer = await draftAnswer(e.message); // an LLM call, a few seconds
// Reply tokens die after one minute and work once. On a retried
// delivery the token is already dead, so fall back to push instead
// of dropping the answer on the floor.
try {
await lineReply(e.replyToken, answer);
} catch {
await linePush(e.source.userId, answer);
}
}
return new Response("ok", { status: 200 });
}よくある質問
- コンソールの「検証」ボタンが失敗し続けます。何が必要ですか?
- 「検証」ボタンは空の events 配列を含む POST を URL に送り、https で 200 だけが返ることを期待します。ローカルのトンネルは通ったり通らなかったりし、ホスト名が変わると壊れます。AnyHook の受信 URL は安定した https エンドポイントで、リクエストを永続キューに入れてから応答するので、「検証」は一度で通り、その後も通り続けます。もう 1 つ知っておくべき点があります。LINE は検証用の POST にも、他の Webhook と同じくチャネルシークレットで署名します。送信元を LINE にしているのに「検証」が失敗するなら、たいていの原因はシークレットの入力ミスです。AnyHook はそうしたリクエストをエッジで 401 として拒否します。App のページにその件数が表示され、毎日最初の 5 件はヘッダーも含めて拒否済みとしてイベントログに残るので、失敗の内容を確認できます。シークレットを貼り直して、もう一度「検証」を押してください。
- x-line-signature を検証するのは AnyHook とサーバーのどちらですか?
- App の送信元が LINE で、チャネルシークレットが設定されていれば、AnyHook がエッジで検証します。検証に失敗したリクエストは、サーバーに届く前、クォータを消費する前に拒否されます。サーバー側では、統一された AnyHook-Signature ヘッダーを 1 つ検証するだけです。元の x-line-signature もそのまま転送されます。LINE の方式はタイムスタンプなしでボディだけに署名するので、リトライ時にも有効なままです。下流で二重にチェックしたい場合にも使えます。
- 配信がリトライされたとき、応答トークンはどうなりますか?
- ほぼ確実に期限切れです。トークンの有効期間は元の Webhook から 1 分です。うまくいくのは、速く済む最初の試行では応答メッセージで返信し、応答メッセージの送信に失敗したらプッシュメッセージに切り替えるパターンです。どちらの場合も AnyHook はイベントのボディ全体を保存しているので、遅れて動いたハンドラーでも何が言われたかを正確に把握できます。
- ボットサーバーが無料プランでスリープします。それでも返信は間に合いますか?
- いずれにしても LINE は AnyHook から約 1 秒で 200 を受け取るので、LINE 側では失敗になりません。AnyHook はすぐにイベントをサーバーに送り、サーバーが起きるのを待ちます(Free プランで最大 60 秒)。その間に応答すれば、イベントは数秒遅れで最初の試行で届き、元のイベントから 1 分間有効な応答トークンもたいていはまだ使えます。起動にトークンの 1 分より長くかかる場合は、上のコードにあるプッシュメッセージへのフォールバックが代わりに返信します。サーバーが時間内にまったく応答しなければ、次の試行は 5 分後です。
- ボットを Python で書いています。AnyHook-Signature はどう検証すればよいですか?
- ヘッダーの形式は t=<unix seconds>,v1=<hex> です。転送先の署名シークレット(ダッシュボードの App → Destinations → Signing secret)で f"{t}.{raw_body}" の HMAC-SHA256 を計算し、hmac.compare_digest で v1 と比較し、5 分より古い t は拒否します。ハンドラーですでに x-line-signature を検証しているなら、代わりにそれを続けても構いません。LINE はボディだけに署名するので、リトライ時にも有効なままです。
- AnyHook がリトライしてくれるなら、LINE の「Webhookの再送」も有効にするべきですか?
- はい。両者はそれぞれ別の区間を担当します。AnyHook はイベントを保存した後、AnyHook からサーバーまでの区間をリトライします。LINE の「Webhookの再送」は、LINE から AnyHook までの区間を担当します。これが効くのは、AnyHook がイベントを永続化できず、送信元にリトライしてもらうために意図的に 503 を返すという、まれなケースです。
変えるのは URL 1 つだけ。LINE のハンドラーはそのまま。
Free プランには月 10,000 件が含まれます。SDK は不要です。LINE の Webhook を AnyHook の受信 URL に向けるだけです。