概要
line-bot-sdk-javaを使ってLINE BOTを作ったが、たまにInvalid reply tokenが発生し返信できないことがある。
この記事ではJavaの場合の解決方法を書くが、他の言語でも原因は同じである。
ちなみに、Herokuの無料hobbyプランを使っている。
エラー内容
ERROR 4 --- [io-33152-exec-3] c.l.b.s.b.s.LineMessageHandlerSupport : InvocationTargetException occurred.
Caused by: java.lang.RuntimeException: java.util.concurrent.ExecutionException: com.linecorp.bot.client.exception.BadRequestException: Invalid reply token : ErrorResponse(requestId=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, message=Invalid reply token, details=[])
Caused by: java.util.concurrent.ExecutionException: com.linecorp.bot.client.exception.BadRequestException: Invalid reply token : ErrorResponse(requestId=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, message=Invalid reply token, details=[])
Caused by: com.linecorp.bot.client.exception.BadRequestException: Invalid reply token : ErrorResponse(requestId=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, message=Invalid reply token, details=[])
原因
公式のドキュメントには以下のように書いてある。
応答トークンは一定の期間が経過すると無効になるため、メッセージを受信したらすぐに応答を返す必要があります。応答トークンは1回のみ使用できます。
応答メッセージを送る
LINE BOTのreplyMessage(応答メッセージ)は、送信されたメッセージ1つに付きreply tokenが発行され、それを使ってBOTが返信するという仕組みになっている。そして、このreply tokenは一定期間経過すると無効になり、そうするとInvalid reply tokenが発生し返信ができない、というのが原因のようである。
以下の記事で、このtokenがどのくらいの時間で無効になるのか検証されているが、30秒らしい。
エラーが起こる条件
原因はわかったが、なぜこのエラーが発生するのか。
このBOTはHerokuの無料・ホビーのプランで運用している。
https://jp.heroku.com/pricing
この無料のプランは、インスタンスに30分アクセスがない場合自動でsleep状態になる。
そして再度アクセスがあった場合に、起動し処理を実行する。
sleep状態であっても、受けたリクエストに対する処理は実行してくれるのだが、sleepからrunning状態になるのに時間がかかるため、sleep開けの初回リクエストに対するレスポンスは、通常よりも少し時間がかかってしまう。
つまり今回のエラーは、LINEでユーザーがBOTにメッセージを送った際、Herokuがsleep状態であるとHerokuが起動する時間がかかるため、その分BOTからの返信に時間がかかる。その間にreply tokenの有効期限が過ぎてしまうので、Invalid reply tokenが発生するということになる。
対策
対策には2つある。
1. Herokuを有料プランにする
有料プランにすればsleep状態にはならないので、お金で解決する。
https://jp.heroku.com/pricing
2. pushMessageを使う
Invalid reply tokenが発生したら、pushMessageを使って再送信する方法で解決する。
LINE BOTでメッセージを送信する方法には、大きく分けて、先述したreplyMessage(応答メッセージ)と、このpushMessage(プッシュメッセージ)の2つがある。
replyMessageはreply tokenが必要だが、pushMessageにはreply tokenは不要なので、任意のタイミングでメッセージを送信することができる。
つまり、replyMessageでInvalid reply tokenが発生したら、pushMessageを使ってリトライする、という方法で解決する。
ちなみにpushMesasgeにはreply tokenは不要だが、グループに返信する場合にはgroupId、個別ユーザーへの場合はuserIdが必要なので、送信する際にはいずれかを指定すること。
@Slf4j
@LineMessageHandler
public class KitchenSinkController {
// 一部省略
// replyMessageでの返信
private void replyMessage(String replyToken, List<Message> replyMessage) throws Exception {
try {
// 返信処理
BotApiResponse apiResponse = lineMessagingClient
.replyMessage(new ReplyMessage(replyToken, replyMessage, this.notificationDisabled))
.get();
log.info("Sent messages: {}", apiResponse);
} catch (InterruptedException | ExecutionException e) {
//throw new RuntimeException(e);
// Invalid reply tokenが発生した場合ここでcatchされるので、pushMessage処理を実行する。
// toにはuserIdもしくはgroupIdを入れる(userId、groupIdをプロパティにsetする処理は省略)
String to = (this.groupId.isEmpty()) ? this.userId : this.groupId;
pushMessage(to, replyMessage);
}
}
// pushMessageでの送信。toにはuserIdもしくはgroupIdを入れる
private void pushMessage(String to, List<Message> replyMessage) throws Exception {
try {
BotApiResponse apiResponse = lineMessagingClient
.pushMessage(new PushMessage(to, replyMessage, this.notificationDisabled))
.get();
log.info("Sent push messages to: {}", to);
log.info("Sent push messages: {}", apiResponse);
} catch (InterruptedException | ExecutionException e) {
// pushMesageでもだめならExceptionを投げる
throw new RuntimeException(e);
}
}
}
ちなみにuserIdは以下で取得できる。
String userId = event.getSource().getUserId();
groupIdはグループチャットでない場合は存在しないため、取得方法が少し面倒だがこんな感じ。
String groupId;
if (event.getSource() instanceof GroupSource) {
groupId = ((GroupSource) event.getSource()).getGroupId();
} else {
groupId = "";
}
pushMessageの詳細については以下の公式ドキュメントを参照。
- https://developers.line.biz/ja/docs/messaging-api/sending-messages/#methods-of-sending-message
- https://developers.line.biz/ja/reference/messaging-api/#send-reply-message
まとめ
この記事ではJavaの解決策を書いたが、他の言語でも原因は同じため、pushMessageでリトライするようにすれば解決できる。