ThreadsとXのアクセストークンをcronで更新する:fetch failedと本当の失効の見分け方
SNSへ代行投稿するサービスでは、アクセストークンを期限内に更新し続ける仕組みが欠かせません。私のサービス「sns-poster」では、cronで定期的に更新しています。この記事では、更新失敗の通知が来たときに「本当に失効したのか、一時的な通信障害なのか」を見分ける方法と、通知設計の考え方を書きます。
なぜcronで更新するのか
- Threads: 長期アクセストークンは、期限切れの前に更新する必要があります。仕様は変更されることがあるため、公式ドキュメントで最新の内容を確認してください。
- X(OAuth 2.0): アクセストークンは、リフレッシュトークンで更新します。仕様は変更されることがあるため、公式ドキュメントで最新の内容を確認してください。
更新に失敗すると、次の投稿が失敗します。そのため、失敗の通知は必須です。
起きたこと:fetch failed のメール
ある日、次のようなメールが届きました。
[システム名] Xトークンのリフレッシュに失敗しました
エラー: fetch failed
refresh_tokenが無効化されている可能性があります。再認可してください。
文面どおりに再認可しようとしましたが、その前に原因を確認しました。結果は、cron実行時のDNSの一時的な失敗(EAI_AGAIN)で、トークン自体は有効でした。再認可は不要でした。
fetch failed とは何か
Node.jsの fetch(undici)が出す fetch failed は、相手から応答が返ってこなかったときの汎用的なエラーです。本当の理由は error.cause に入っています。
| 見え方 | 意味 | 対処 |
|---|---|---|
fetch failed(causeが ENOTFOUND / EAI_AGAIN) | DNSが引けない | 一時的。リトライ |
fetch failed(causeが ETIMEDOUT / UND_ERR_CONNECT_TIMEOUT) | 接続できない | 一時的。リトライ |
fetch failed(causeが ECONNRESET) | 接続を切られた | 一時的。リトライ |
HTTP 400 + invalid_grant など | 相手が明確に拒否した | トークンが無効。再認可 |
見分け方の基本は、HTTPの応答が返ってきたかどうかです。応答が返っていれば、ステータスと本文に理由があります。返っていなければ、通信の問題です。
通知の設計
メールの「無効化されている可能性があります」という文面は、失敗の種類にかかわらず出る定型文でした。これが誤解のもとでした。次のように分けるのが妥当です。
- ネットワーク系のエラー: 一定間隔で数回リトライする。連続して失敗したときだけ通知する
- **明確な拒否(
invalid_grantなど)**: 待っても直らないので、即時に通知して再認可を案内する - 通知の文面: 断定しない。「通信エラーの可能性」と「再認可が必要な可能性」を、失敗の種類に応じて書き分ける
ログに残すもの
原因の切り分けには、次の3つをログに残します。
err.cause(通信エラーの本当の理由)- HTTPステータス
- レスポンス本文(トークンやシークレットは必ずマスクする)
fetch failed の一言だけでは、後から調べられません。
更新の「ローテーション」にも注意
Xのリフレッシュトークンは、更新のたびに新しいものに置き換わります。次の場合は、本当に invalid_grant になります。
- 更新に成功したが、新しいトークンの保存に失敗した
- 更新処理が同時に2回走った
通信障害と見分けるには、更新の成功と保存を一続きの処理として扱い、同じ連携に対する更新が重ならないようにしておく必要があります。
まとめ
fetch failedは、まずerror.causeを見る。トークンの失効とは限らない- HTTPの応答が返っていなければ通信の問題、返っていれば本文の理由に従う
- ネットワーク系はリトライしてから通知し、明確な拒否は即時に通知する
- ログには
err.cause、ステータス、本文(マスク済み)を残す
sns-posterは、X・Threads・Instagram・LinkedIn・Facebookへ予約投稿できるサービスです。詳しくは機能ページをご覧ください。