【GAS】LINE Messaging APIのエラー対処法|Webhookイベントオブジェクト送信時にタイムアウトが発生しました

LINE
記事内に広告が含まれていることがあります。

GAS(Google Apps Script)とLINE Messaging APIでWebhookを使ってPOSTで受け取ったデータを処理するプログラムを作成したときに、以下のようなエラーが発生することがあります。

エラーの内容

Webhookイベントオブジェクト送信時にタイムアウトが発生しました

ここではこの原因と対処法を解説しています。



発生原因

このエラーが発生する原因は、WebhookでdoPost()を実行した場合、doPost関数の処理に時間がかかりすぎているためです。

例えば以下のようなプログラムがあるとします。

イベントを発生させたユーザーのユーザー名やプロフィール画像のURLを取得して、スプレッドシートに書き出す処理は成功します。

ところが、その次の「sendFirstMail()」という関数を実行するところでアクセス権がなく、タイムアウトのエラーが発生します。

//ssid, channelAccessToken //グローバル変数で定義

// LINEのWebhookからイベントデータを受信する
function doPost(e) {
  let sheetName = 'LINEフォローリスト';
  const eventData = JSON.parse(e.postData.contents);
  // スプレッドシートのシートを取得
  const sheet = SpreadsheetApp.openById(ssid).getSheetByName(sheetName);

  // イベントごとに処理を行う
  eventData.events.forEach(event => {
    const eventType = event.type;  // イベントタイプ(例: message, follow, unfollowなど)
    const userId = event.source.userId || '不明なユーザー';
    let eventContent = '';

    // 指定のイベントタイプのみ処理
    if (eventType === 'follow') {
      eventContent = '友だち追加';
    } else if (eventType === 'unfollow') {
      eventContent = '友だち削除';
    } else if (eventType === 'join') {
      eventContent = 'グループ参加';
    } else if (eventType === 'leave') {
      eventContent = 'グループ退出';
    } else {
      // その他のイベントは処理しない
      return;
    }

    let userProfile;
    let displayName, pictureUrl;

    // ユーザープロファイルを取得
    // unfollowではユーザープロファイルを取得できないため、スキップ
    if (eventType === 'unfollow') {
        userProfile = { displayName: '削除済みユーザー', pictureUrl: '' };
    }else{
        userProfile = getUserProfile(userId);
        displayName = userProfile.displayName;
        pictureUrl = userProfile.pictureUrl;
    }
    // スプレッドシートに書き出す
    sheet.appendRow([new Date(), userId, displayName, pictureUrl, eventType, eventContent]);
  });

  

  //初回メッセージ送信を実行
  sendFirstLINE() //ここでアクセスできずタイムアウトが発生
}

// LINE APIからユーザー情報を取得
function getUserProfile(userId) {
  try{
    const url = `https://api.line.me/v2/bot/profile/${userId}`;
    const options = {
      "headers": {
        "Authorization": "Bearer " + channelAccessToken
      },
      "method": "GET",
    };

    const response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText());
  }
  catch{console.log(e)}
}




対処法

対処法は、重い処理をトリガーでセットしてしまうことです。

つまり、doPost関数自体の処理はトリガーの設定のみとなります。また、トリガーの実行が完了したら、そのトリガーを削除するようにします。

タイムアウトの対処法
  1. doPostの中でトリガーをセットする
  2. 対象の関数の中でトリガーを解除する


プログラムを変更したら、新しいデプロイとWebhook URLのアップデートをお忘れなく。


トリガーのセット

例えば、上記のようにsendFirstLINE関数を実行したい場合は、sendFirstLINE()の部分を以下のように変更します。

    //トリガーを作成(Webhookがタイムアウトするため)
    ScriptApp.newTrigger('sendFirstLINE')
      .timeBased()
      .after(1 * 1000)  // 1秒後にセット
      .create();


ScriptApp.newTrigger(関数名)で、指定した関数をトリガーにセットします。

timebasedの後に時間を指定することで、いつ実行するかを指定します。ここでは、after(1 * 1000)で1000ms後、つまり1秒後を指定しています。

最後に、create()でトリガーをセットしています。

注意点

上記トリガーは1秒後にセットしていますが、実際のトリガーは1分後に設定されています。

GASのドキュメントによると、設定した時間になるとは限らないが、それ以下になることはないとの記述があります。

https://developers.google.com/apps-script/reference/script/clock-trigger-builder?hl=ja


(参考)Google公式 トリガーの設定で使えるメソッド|Class ClockTriggerBuilder



トリガーの削除

このままではPOSTの送信がある度にトリガーが無限に増え続けてしまいます。時間を指定しているので実行は1度きりですが、トリガーが分かりにくくなってしまいます。

そこで、指定の関数を実行したらトリガーを削除する処理を追加します。

ScriptApp.getProjectTriggers()で全てのトリガーを取得して、getHandlerFunction()で関数名が指定した関数のときに、ScriptApp.deleteTrigger()関数でトリガーの削除を行います。


例えば、トリガーでセットした関数が「SendFirstLINE」であれば、以下のようにします。

  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'sendFirstMail') {
      ScriptApp.deleteTrigger(trigger);
    }


こうすることでトリガーをセットすることができます。


中身を確認すると日時がセットされています。

MEMO

エラー通知設定はGASから変更することはできません。



注意点

上記のようにWebhookの実行でトリガーをセットすることで、LINE Messaging API経由で関数を実行することができるようになります。

ところがこの状態でも、検証を実行すると以下のようにタイムアウトが発生します。


ただ、トリガーは問題なくセットできているので、ここではこれで良しとしておきます。

例えば、3回「検証」をクリックすると、3回分トリガーが設定されます。



スプレッドシートのアクセス権は関係ない

当初、スプレッドシートのアクセス権の問題かと思いましたが、アクセス権は関係ありません。

「制限付き」で問題なくプログラムを実行することができます。

Point

シートのアクセス権限を「リンクを知っている全員」「編集可能」にする必要はありません。

タイトルとURLをコピーしました