slack boltでリトライリクエストに対応

JavaScript

Slackのapiでは3秒以内にレスポンスを返すことが求められます。3秒以内に返せない場合、失敗とみなして3回までリトライされます。AWS LambdaやCloud FunctionなFaasで実行する場合は3秒を保証できない可能性があるため、意図せず複数回実行される可能性があり、リトライリクエストを正しく捌く必要があります。

対処法としては大きく3つ考えられます。

  • リトライの発生を抑制する(http headerにX-Slack-No-Retry: 1を付与する)
  • リトライ時にも安全となるように常に実行すべきか確認するロジックを組む
  • リトライは全て握りつぶす

1つ目に関しては3秒以内にレスポンスを返せないリスクを考えているため、きちんとレスポンスできるようならそもそも問題にならないため除外。
2つ目は丁寧な解決方法だと思うが、実装が手間なので程度問題ではあるが避けれるなら避けたい。

ということで3つ目の方法を取ります。1発目のリクエストを遅れながらfunctionが処理するため、リトライリクエストであれば全部握りつぶして200のstatusを返して終わりにします。

簡単に検索してみたところ、リクエストヘッダーに以下の項目でリトライかどうか判断できそうです。

headers['X-Slack-Retry-Num']
headers['X-Slack-Retry-Reason']
Events API
TheEventsAPIisasubscription-basedsystemthatsendsyourappHTTPrequestswheninterestingstuff.ItreplacestheRealTimeMessagingAPI.

headerってどうやって受け取るの?

ヘッダーに情報があることは分かったのですが、よくある以下のようなコード内からheaderにたどり着く方法が分かりません。typescriptでargsの型を見る限りではheader情報は持ってない感じ。

app.message('hello', async ({ message, say, context }) => {})

デフォルトの状態では持ってないのでcustomPropertiesExtractorでcontextに値を渡すのが正解のようだ。cloud functionsでの例になりますが、以下のような感じでrecieverを初期化すればcontext.headersに値が渡ります。

const expressReceiver = new ExpressReceiver({
  signingSecret: signingSecret.value(),
  endpoints: '/events',
  processBeforeResponse: true,
  customPropertiesExtractor: (req) => {
    return {
      headers: req.headers,
      foo: 'bar',
    };
  },
});
Slack | Bolt for JavaScript
AframeworkthatmakesSlackappdevelopmentfastandstraight-forward.WithasingleinterfaceforSlack’sWebAPI,EventsAPI,andinteractivefeatures,Boltgivesyouthefullpowerofth...

receiver毎の具体的なサンプルはこちらを参照。

bolt-js/examples/custom-properties at main · slackapi/bolt-js
AframeworktobuildSlackappsusingJavaScript.Contributetoslackapi/bolt-jsdevelopmentbycreatinganaccountonGitHub.
app.view('dialog_submit', async ({ ack, client, context, body }) => {
  if (
    context.headers &&
    context.headers['X-Slack-Retry-Num'] &&
    context.headers['X-Slack-Retry-Reason'] === 'http_timeout'
  ) {
    await ack();
    return;
  }
}

こんな感じでリトライを無視したいendpointで適宜判定すればOK.

endpoint毎ではなく、常にリトライを無視したい場合はこんな感じでmiddlewareで返してしまえば個別に書く必要はなくなります。

app.use(async ({ context, next, ack }) => {
  if (
    context.retryNum &&
    context.headers['X-Slack-Retry-Reason'] === 'http_timeout'
  ) {
    await ack?.();
    return;
  }

  await next();
});

コメント