【前回記事の補足】Pythonのline-bot-sdkで作ったBotが、Webhook URLのVerifyに失敗する件を調査した


この記事について

前回記事「Python(Flask)で実装したLINEBotを"Herokuを使わずに"動かす」の補足。
Pythonで実装したLINE BotをLINE DevelopersのコンソールにてWebhook URLに指定し、verifyしようとするとエラートなってしまう件が
前回記事の執筆時点で未解決だったので、その件について調査した。

調査

前回記事で作成したLINE Botは、LINEサーバから送信されてきたリクエストボディをログに出力するようにしているので
verifyを実施した際、どのようなリクエストが送られてくるかを確認した。

2020-05-13 02:24:52,822 INFO Request body: {
  "events": [
    {
      "replyToken": "00000000000000000000000000000000",
      "type": "message",
      "timestamp": 1589304292797,
      "source": {
        "type": "user",
        "userId": "Udeadbeefdeadbeefdeadbeefdeadbeef"
      },
      "message": {
        "id": "100001",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "ffffffffffffffffffffffffffffffff",
      "type": "message",
      "timestamp": 1589304292797,
      "source": {
        "type": "user",
        "userId": "Udeadbeefdeadbeefdeadbeefdeadbeef"
      },
      "message": {
        "id": "100002",
        "type": "sticker",
        "packageId": "1",
        "stickerId": "1"
      }
    }
  ]
}

見たところ、ユーザがテキストメッセージとスタンプを送信した際に送られるものを模しているようだ。
ちなみに本当にユーザがテキストメッセージとスタンプを送信した際に送られるリクエストボディは以下。

2020-05-12 16:44:27,427 INFO Request body: {"events":[{"type":"message","replyToken":"4f0255538cb243d7811329cc37a6cd8a","source":{"userId":"*********************************","type":"user"},"timestamp":1589269467107,"mode":"active","message":{"type":"text","id":"11950285594485","text":"テスト"}}],"destination":"*********************************"}
2020-05-12 16:44:39,605 INFO Request body: {"events":[{"type":"message","replyToken":"0490eef3682947e7a0ca6c37af2b9058","source":{"userId":"*********************************","type":"user"},"timestamp":1589269479562,"mode":"active","message":{"type":"sticker","id":"11950286543191","stickerId":"263","packageId":"4","stickerResourceType":"STATIC"}}],"destination":"*********************************"}

そしてエラー発生時のログは以下。

2020-05-13 02:24:52,884 ERROR Exception on /callback [POST]
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1951, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1820, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1949, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1935, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "./app.py", line 49, in callback
    handler.handle(body, signature)
  File "/usr/local/lib/python3.6/site-packages/linebot/webhook.py", line 260, in handle
    func(event)
  File "./app.py", line 59, in handle_message
    TextSendMessage(text=event.message.text))
  File "/usr/local/lib/python3.6/site-packages/linebot/api.py", line 107, in reply_message
    '/v2/bot/message/reply', data=json.dumps(data), timeout=timeout
  File "/usr/local/lib/python3.6/site-packages/linebot/api.py", line 996, in _post
    self.__check_error(response)
  File "/usr/local/lib/python3.6/site-packages/linebot/api.py", line 1022, in __check_error
    error=Error.new_from_json_dict(response.json)
linebot.exceptions.LineBotApiError: LineBotApiError: status_code=400, request_id=3183fc14-5946-4562-8c26-9b5109126a67, error_response={"details": [], "message": "Invalid reply token"}, headers={'Server': 'nginx', 'Date': 'Tue, 12 May 2020 17:24:52 GMT', 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'x-line-request-id': '3183fc14-5946-4562-8c26-9b5109126a67', 'x-content-type-options': 'nosniff', 'x-xss-protection': '1; mode=block', 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', 'pragma': 'no-cache', 'expires': '0', 'x-frame-options': 'DENY'}

どうやら「リクエストに含まれるreply_tokenが不正」ということでエラーになっている模様。
line-bot-sdk-pythonのソースを追いつつ考察したが、LINEサーバから送られてきたリクエストをもとに
LINEのWeb APIへのリクエストを作成して送信し、そのレスポンスを解析して正常処理されたかどうかを確認している模様。
LINEのWeb API側で"00000000000000000000000000000000"および"ffffffffffffffffffffffffffffffff"というreply_tokenが不正とみなされているのだろうか。

対応

前回記事で作成したapp.pyの、テキストメッセージとスタンプの受信をハンドリングしている関数を修正。
reply_tokenが特定の値だった場合に、処理を打ち切って200を返すようにした。

app.py
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):

    # reply_tokenが特定の値だった場合に処理終了
    if event.reply_token == "00000000000000000000000000000000":
        return

    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text))

@handler.add(MessageEvent, message=StickerMessage)
def handle_video(event):

    # reply_tokenが特定の値だった場合に処理終了
    if event.reply_token == "ffffffffffffffffffffffffffffffff":
        return

    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text="Sticker"))

これで試してみたところ、LINE Developersのverifyでエラーが出なくなった。

懸念点としては、ユーザが普通にテキストメッセージやスタンプを送信した際に設定されるreply_tokenが
"00000000000000000000000000000000"および"ffffffffffffffffffffffffffffffff"となるケースが本当に有り得ないのかどうか。
もし有り得るとするなら、これらのreply_tokenが送信されてきた場合にユーザに対して返信することができなくなってしまう。
LINEのWeb API側で、これらのreply_tokenを不正とみなしているようなのでないとは思うが…。

おわりに

line-bot-sdk-pythonを使用している限り、今回のようなverifyの際に送信されるリクエスト専用の処理を入れない限り
verifyは絶対に成功しない、ということになると思います。
これはどないやねん、と思うのですが、どうやらこういうものであるようです。

ちなみに、私は今から数年前にもJavaのSpring BootでLINE Botを作成しているのですが
その際にはこのようにverifyが失敗するというようなことは起こらなかったと記憶しています。
Python固有の問題なのか、数年で状況が変わったのかは分かりません。

私の記載した情報に間違い等あれば教えていただけるとありがたいです。