
このとき、各リクエストがそれぞれ独立してトークン再発行APIを呼び出すと、単に通信回数が増えるだけでは済みません。リフレッシュトークンローテーションを採用しているバックエンドでは、最初の再発行によって古いリフレッシュトークンが無効化され、後続の再発行が失敗することがあります。
Axiosのインターセプターには、トークンを自動で付けたり、401を検知したりする仕組みを組み込めます。しかし、複数の非同期処理を安全に調停する機能が[Axiosに標準で備わっているわけではありません。私たちが isRefreshing](/articles/laravel-11nodeferguan-shuhacong/) のような状態管理と、待機中のリクエストを保持するキューを設計する必要があります。
この記事では、Axiosインターセプターによるトークン再発行の仕組みを、リクエストが並行処理される瞬間の内部挙動から順番に整理していきます。なぜフラグだけでは足りないのか、なぜキューに resolve と reject を保存するのか、そして無限ループや二重更新をどう避けるのかまで、実装の意図を確認してみましょう。
401が同時に返ると、再発行処理が競合する
まず、一般的なアクセストークン認証の流れを確認します。
1. ブラウザがアクセストークンを付けてAPIを呼び出す
2. バックエンドがアクセストークンを検証する
3. トークンが有効ならデータを返す
4. 期限切れなら 401 Unauthorized を返す
5. フロントエンドがリフレッシュトークンを使って新しいアクセストークンを取得する
6. 失敗したAPIリクエストを、新しいアクセストークンで再試行する
リクエストが1本だけなら、この流れは比較的単純です。レスポンスインターセプターで401を検知し、再発行APIを呼び、成功したら元のリクエストを再実行すれば動きます。
問題は、同じ瞬間に複数のリクエストが走っている場合です。
たとえば、画面表示のために次の3つのAPIを同時に呼び出しているとします。
- ユーザー情報を取得するAPI
- 通知一覧を取得するAPI
- ダッシュボードの集計値を取得するAPI
これらがすべて期限切れのアクセストークンを使って送信されると、バックエンドからは3本とも401が返ります。Axiosのレスポンスインターセプターは、それぞれのレスポンスを個別に受け取るため、何も制御しなければ3つの処理が次のように進みます。
| 時点 | リクエストA | リクエストB | リクエストC |
|---|---|---|---|
| 1 | 古いアクセストークンで送信 | 古いアクセストークンで送信 | 古いアクセストークンで送信 |
| 2 | 401を受信 | 401を受信 | 401を受信 |
| 3 | 再発行APIを送信 | 再発行APIを送信 | 再発行APIを送信 |
| 4 | 新しいトークンを受信 | 再発行に失敗する可能性 | 再発行に失敗する可能性 |
| 5 | 元のAPIを再試行 | ログアウト処理などに進む可能性 | ログアウト処理などに進む可能性 |
この状態がレースコンディションです。どの処理が先に完了するかによって、最終的な結果が変わります。
トークン再発行で守るべきなのは、401を受け取った回数ではなく、再発行処理を同時に何本走らせるかです。
非同期処理では、コードの上から順に終わるとは限らない
JavaScriptのコードは上から読まれますが、非同期通信の完了順序はコードの記述順には固定されません。
最初に401を受け取った処理が再発行APIを呼んだとしても、その通信が完了する前に、別のリクエストが401を受け取ることがあります。すると、2つ目の処理も「まだ新しいアクセストークンがない」と判断し、再発行APIを呼び出してしまいます。
ここで注意したいのは、単純な変数の読み書きだけでは処理の順番を保証できないことです。
たとえば、処理の流れが次のようになっているとします。
1. isRefreshing を確認する
2. false なので再発行処理を開始する
3. isRefreshing = true にする
この間に別の非同期コールバックが実行される可能性があると、2つの処理が同じ判定を通過することがあります。そのため、再発行を開始する直前にフラグを立てること、そして再発行処理の成功・失敗にかかわらず最後にフラグを戻すことが必要です。
401なら何でも再発行してよいわけではない
実装を始めると、レスポンスステータスが401ならすべて再発行処理へ進めたくなります。しかし、401の意味は必ずしも「アクセストークンの期限切れ」とは限りません。
たとえば、次のようなケースがあります。
- ログインしていない状態で保護されたAPIを呼んだ
- リフレッシュトークン自体が無効になっている
- ユーザーが管理画面からセッションを失効させた
- 認証情報の形式が壊れている
- 再発行APIそのものが401を返している
特に最後のケースは重要です。再発行APIの401を同じインターセプターが拾うと、再発行のために再発行を試みる循環が生まれます。
そのため、実際の制御ではステータスコードだけでなく、元のリクエストがどのエンドポイントへ向いているか、すでに再試行済みか、認証エラーの種類を判別できるか、といった条件も組み合わせます。
isRefreshing は再発行処理の入口を一つにする
並行リクエストを制御する基本が isRefreshing フラグです。
このフラグは、「現在、アクセストークンの再発行処理が進行中か」を表します。値が false のときだけ再発行APIを呼び出し、すでに true なら新しいリクエストを発生させず、現在の再発行が終わるまで待機させます。
処理の考え方は次のようになります。
- 401を受信する
- 再試行済みか確認する
isRefreshingがfalseなら、再発行処理の担当になるisRefreshingがtrueなら、再発行処理の完了を待つ- 再発行に成功したら、待機中のリクエストを新しいトークンで再試行する
- 失敗したら、待機中のリクエストも失敗として処理する
- 最後に
isRefreshingをfalseに戻す
ここでのポイントは、フラグを「再発行が必要かどうか」の判定に使うのではなく、「再発行を担当している処理が存在するか」の判定に使うことです。
フラグだけでは待機中のリクエストを救えない
isRefreshing を導入すると、重複した再発行APIの呼び出しは防げます。しかし、フラグだけでは、後から401を受け取ったリクエストをどのように再開するかが決まりません。
たとえば、リクエストBが次のような処理をしたとします。
1. isRefreshing === true を確認する
2. 何らかの方法で処理を待つ
3. 再発行完了後に新しいトークンを取得する
4. 元のリクエストを再試行する
この「何らかの方法で処理を待つ」を担うのが failedQueue です。
もし待機処理を用意せずに、単に「再発行中なので失敗として返す」とすると、最初のリクエストだけが救済され、同時に401になった他のリクエストはそのまま画面エラーになります。逆に、待機中の処理を保存していなければ、再発行成功を通知する相手も存在しません。
フラグを戻すタイミングに注意する
よくある失敗は、再発行APIの成功時だけ isRefreshing = false にする実装です。
再発行が失敗した場合、フラグが true のまま残るため、以降の401リクエストが永遠に「再発行中」と判定されることがあります。画面を再読み込みするまで認証処理が復旧しない場合もあるでしょう。
再発行の成功・失敗を問わず、後処理を必ず実行できるように、finally に相当する終了処理を設計しておく必要があります。
処理の順序としては、次の形が安定します。
1. 再発行を開始する前に isRefreshing = true
2. 成功時は新しいトークンを保存する
3. 待機中のキューを成功として解放する
4. 失敗時は待機中のキューを失敗として解放する
5. 最後に isRefreshing = false
6. 必要ならログアウトやログイン画面への遷移を行う
ログアウト処理を先に実行してしまうと、待機中のリクエストが古い状態を参照したり、画面遷移と再試行が競合したりする場合があります。認証状態をどの順番で更新するのか、プロセスとして整理しておきましょう。
failedQueue はPromiseの解決と失敗を保留する
failedQueue は、401を受け取ったものの、現在は再発行を待つしかないリクエストを保持する配列です。
ただし、保存するのはAxiosのリクエストそのものではありません。一般的には、再発行が成功したときに新しいトークンを受け取る resolve と、失敗したときにエラーを返す reject を保存します。
待機中のリクエストは、次のようなPromiseを返す形になります。
- 再発行が成功したら
resolve(newToken) - 再発行が失敗したら
reject(error)
そのPromiseをレスポンスインターセプター内で待ち、解決されたトークンを使って元の設定情報を再利用します。
この仕組みによって、各リクエストは「自分で再発行APIを呼ぶ」のではなく、「再発行を担当している処理から結果を受け取る」ことができます。
キューが動く内部の流れ
キューを使った場合、同時に401になったリクエストの動きは次のように変わります。
1. リクエストAが401を受け取る
2. isRefreshing が false なので、Aが再発行処理を担当する
3. リクエストBが401を受け取る
4. isRefreshing が true なので、Bはキューに入る
5. リクエストCも同様にキューに入る
6. Aの再発行APIが成功する
7. 新しいアクセストークンを保存する
8. キュー内のBとCへ新しいトークンを通知する
9. BとCが元のAPIリクエストを再試行する
10. キューを空にする
このとき、BとCは再発行APIのレスポンスを直接処理しているわけではありません。キューを通じて、Aが取得したトークンを受け取っています。
ここで「キュー」という名前から、リクエストが必ず登録順に1本ずつ再送されると考えてしまうことがあります。しかし、実際には resolve が呼ばれた時点で待機していたPromiseが解決され、それぞれの再試行処理が動き始めます。厳密な逐次実行が必要なら、別途そのための制御が必要です。
成功時だけでなく失敗時にも全件を解放する
再発行に失敗したとき、キュー内のPromiseを放置してはいけません。
失敗したリクエストをキューに入れたままにすると、そのリクエストは永遠に解決されず、ローディング表示が消えない、画面の処理が止まる、コンポーネントが不要な状態を保持し続ける、といった問題につながります。
再発行APIが失敗したら、キュー内のすべての reject を呼び出し、待機中の処理にも同じ失敗を伝えます。そのうえで、認証状態を破棄し、ログイン画面へ遷移するか、アプリケーション側で統一した認証エラーを表示します。
キューの終了処理は、成功時と失敗時で次のように整理すると理解しやすくなります。
| 再発行APIの結果 | アクセストークン | キューの処理 | 元のリクエスト |
|---|---|---|---|
| 成功 | 保存して利用する | resolve で解放 | 新しいトークンで再試行 |
| 失敗 | 破棄または保持しない | reject で解放 | 認証エラーとして終了 |
| 通信失敗 | 有効性を確認できない | すべて失敗として解放 | 再試行回数を増やさない |
リクエストインターセプターとレスポンスインターセプターの役割を分ける
Axiosのインターセプターは、リクエスト用とレスポンス用で役割を分けると、処理の見通しがよくなります。
リクエストインターセプターは送信前の準備を担当する
リクエストインターセプターでは、保存されているアクセストークンをリクエストヘッダーへ設定します。
ここで行うことは、たとえば次のような処理です。
- 現在保存されているアクセストークンを取得する
Authorizationヘッダーを組み立てる- APIごとに必要な共通ヘッダーを設定する
- 再発行APIやログインAPIに通常のアクセストークンを付けるか判断する
トークンをリクエスト生成時に固定してしまうと、再発行後も古いトークンが使われることがあります。送信直前に最新の保存値を読み取る設計にしておくと、再試行時にも新しいトークンを利用しやすくなります。
ただし、アクセストークンをCookieで送信する構成では、JavaScriptからヘッダーに設定する必要がない場合もあります。保存方法がローカルストレージなのか、メモリなのか、HttpOnly Cookieなのかによって責務は変わります。仕組みを理解しないまま、すべての構成に同じコードを適用しないように注意してください。
レスポンスインターセプターは認証エラーの調停を担当する
レスポンスインターセプターでは、APIが返したエラーを確認し、再発行の対象かどうかを判断します。
ここで重要なのは、受け取ったエラーをすぐに再発行処理へ渡すのではなく、少なくとも次の条件を確認することです。
- HTTPステータスが401か
- 対象のリクエストが再発行APIではないか
- そのリクエストがすでに再試行済みではないか
- ログイン画面への遷移中ではないか
- 現在の認証状態が再発行を許可できる状態か
レスポンスインターセプターは、単なる「401なら再送する関数」ではありません。複数の通信を一つの認証プロセスへまとめる、調停役として考えると設計しやすくなります。
config._retry は無限ループを止めるために使う
トークンを更新した後、元のリクエストを再試行しても401が返る場合があります。
たとえば、次のような状況です。
- 再発行APIが返したアクセストークンがすでに無効だった
- 保存処理に失敗して古いトークンを使い続けている
- APIサーバーと認証サーバーで状態が同期していない
- 権限不足を401で返す設計になっている
- 再発行後のAPIリクエストが別の認証条件に失敗した
このとき、401を受け取るたびに再発行と再試行を繰り返すと、無限ループになります。
そこで、Axiosのリクエスト設定に独自の再試行フラグを持たせます。代表的な名前が config._retry です。
最初に401を受け取った段階で config._retry = true とし、再発行後に同じリクエストを再送します。再送したリクエストがもう一度401を返したら、すでに _retry が真になっているため、再発行処理を行わず、そのままエラーとして終了させます。
このフラグはAxiosが自動的に管理してくれる機能ではありません。プロジェクト側で設定し、インターセプターが参照するための目印です。
再試行は「成功するまで続ける処理」ではなく、「認証状態を一度だけ回復させる処理」として上限を決めておくと、安全に扱えます。
_retry を付ける位置
再試行フラグを付ける位置にも意味があります。
再発行処理を始める前にフラグを設定しておけば、同じ元リクエストが再度インターセプターへ戻ってきても、二度目の再発行を防げます。反対に、再発行成功後や再送直前に設定すると、途中で例外が発生した場合にフラグが付かないまま処理が終わる可能性があります。
ただし、すべてのリクエストへ無条件にフラグを追加するのではなく、対象となる401のリクエスト設定に対してだけ付与します。
また、TypeScriptを使っている場合、Axiosの標準設定型には _retry が存在しないため、型拡張や独自の設定型が必要になることがあります。型エラーを無視して進めるよりも、「これはインターセプター内部で使う独自状態である」と定義しておくほうが、後から処理の意図を追いやすくなります。
リフレッシュトークンローテーションでは重複が致命傷になる
アクセストークンの再発行処理を複雑にする大きな要因が、リフレッシュトークンローテーションです。
この方式では、リフレッシュトークンを使って新しいアクセストークンを取得すると、リフレッシュトークン自体も新しいものへ置き換えられます。つまり、古いリフレッシュトークンを何度も再利用できる前提ではありません。
重複した再発行リクエストが届くと、次のような問題が起こります。
1. 再発行リクエストAが古いリフレッシュトークンを使う
2. サーバーが新しいアクセストークンとリフレッシュトークンを返す
3. サーバー側で古いリフレッシュトークンが無効になる
4. ほぼ同時に再発行リクエストBが同じ古いトークンを使う
5. Bは無効化済みのトークンとして拒否される
6. フロントエンドが認証失敗と判断し、ログアウトへ進む
この場合、実際にはAの再発行が成功しているにもかかわらず、Bの失敗をきっかけにユーザーをログアウトさせることがあります。ユーザーから見ると「普通に操作していただけなのに、突然ログイン画面へ戻された」という分かりにくい障害になります。
そのため、ローテーションを採用している場合は、フロントエンド側で再発行APIを同時に複数回呼ばないことが、単なる性能改善ではなく認証状態を守るための前提になります。
再発行APIを同じインターセプターで処理しない
再発行APIを通常のAxiosインスタンスで呼び出すと、そのレスポンスも同じレスポンスインターセプターを通過します。
再発行APIが401を返したときに、インターセプターが「401だからもう一度再発行」と判断すると、循環が発生します。これを防ぐ方法はいくつかあります。
- 再発行APIのURLを判定して対象外にする
- 再発行専用のAxiosインスタンスを作る
- リクエスト設定に認証更新処理を無効化する独自フラグを付ける
私は、認証処理が複雑になるほど、通常API用と再発行API用のAxiosインスタンスを分ける構成を検討します。インターセプターの条件分岐を増やし続けるより、責務を分離したほうがプロセス全体を追いやすく、テストの再現性も上がるためです。
401検知とタイマー更新を同時に動かさない
アクセストークンの更新方法には、大きく分けて2つあります。
一つは、APIが401を返したときに更新するリアクティブな方式です。もう一つは、トークンの有効期限を見て、期限切れになる前にタイマーなどで更新するプロアクティブな方式です。
どちらにも利点がありますが、両方を導入する場合は競合に注意してください。
たとえば、タイマーが再発行APIを呼び出した直後に、別のAPIリクエストが古いトークンで送信され、401を返すことがあります。すると、タイマー側の再発行処理と、401を検知したインターセプター側の再発行処理が同時に動きます。
リフレッシュトークンローテーションでは、この二重消費が再発行失敗につながります。
更新処理を共通のPromiseにまとめる
この問題を避けるには、401処理とタイマー処理がそれぞれ個別に再発行APIを呼ばないようにします。
現在進行中の再発行処理を、フラグだけではなくPromiseとして保持する方法があります。
考え方としては、次のようになります。
- 再発行処理がなければ、新しいPromiseを作って保持する
- すでにPromiseが存在するなら、そのPromiseを待つ
- 成功したら新しいトークンを返す
- 成功・失敗後に、保持していたPromiseを破棄する
この構成では、401インターセプターもタイマーも、共通の「トークン更新プロセス」を呼び出します。呼び出し元が違っても、実際に走る再発行APIは一つに限定できます。
isRefreshing と failedQueue を使う実装は、401になったリクエストを待たせる設計に向いています。一方、共通Promiseを使う設計は、タイマーなど別の起点から発生した更新処理を一つに束ねるのに向いています。どちらが正解というより、アプリケーションの更新フローに合わせて選ぶことが大切です。
キュー実装で起こりやすい失敗ケース
ここまでの仕組みを理解していても、細部で問題が起こることがあります。私が実装時に特に注意している失敗ケースを整理します。
失敗ケース1:キューを成功時にしか空にしない
再発行成功時にだけキューを空にすると、失敗時にPromiseが残ります。
キューは成功時にも失敗時にも空にする必要があります。配列を空にする処理を個別の分岐に分散させるより、キューを処理する関数を一つ用意し、成功・失敗のどちらからも同じ終了プロセスを通すほうが安全です。
失敗ケース2:新しいトークンを保存する前に再試行する
再発行APIから新しいアクセストークンを受け取ったら、元のリクエストを再試行する前に保存処理を完了させます。
保存前に再試行すると、リクエストインターセプターが古いトークンを読み取る可能性があります。再試行時にヘッダーを直接差し替える実装でも、次のリクエストのためには共通の保存先を更新しておかなければなりません。
アクセストークンをメモリ、Cookie、ローカルストレージなど複数の場所へ保存している場合は、更新順序を決めておきましょう。保存先が複数あると、片方だけが新しくなる不整合も起こりやすくなります。
失敗ケース3:元のリクエスト設定を再利用しすぎる
Axiosの元の設定情報には、古い認証ヘッダーが残っていることがあります。
再試行時に元の設定をそのまま使うと、新しいトークンを保存していても、設定済みの古い Authorization ヘッダーが優先されるケースがあります。再試行前に認証ヘッダーを新しい値へ更新するか、リクエストインターセプターが送信直前に最新値を設定する構成にしておくとよいでしょう。
ただし、設定オブジェクトを不用意に共有・変更すると、同じ設定を参照している処理へ影響することがあります。再試行時は必要な範囲を明示的に更新し、どの値が変更されるのかを追えるようにしておきます。
失敗ケース4:すべての401を同じ意味で扱う
APIによっては、認証失敗と権限不足を同じステータスコードで返すことがあります。
本来はレスポンス本文のエラーコードやAPI仕様も確認し、再発行対象かどうかを判定します。401を受け取ったら必ずトークンを更新する設計は、原因の違うエラーを認証更新へ流してしまうため、トラブルシューティングが難しくなります。
失敗ケース5:コンポーネント側でも個別に再発行する
Axiosインターセプターで認証更新を集中管理している場合、各コンポーネントやAPI呼び出し関数で個別に再発行処理を入れないようにします。
更新処理が複数の場所に存在すると、フラグやキューの状態が共有されず、同じアプリケーション内で複数の再発行プロセスが動き始めます。認証の回復は、できるだけ一つの層に集約したほうが仕組みを説明しやすくなります。
実装を確認するためのテスト観点
この問題は、通常の画面操作だけでは再現しにくいことがあります。アクセストークンが切れた瞬間に、複数のAPIが同時に401を返す状況を意図的に作る必要があります。
まず、次の条件を確認してみましょう。
- 3本以上のAPIリクエストを同時に送ったとき、再発行APIが1回だけ呼ばれる
- 再発行成功後、待機中のすべてのリクエストが再試行される
- 再発行失敗時、待機中のリクエストがすべて終了する
- 再試行したリクエストが再度401になっても無限ループしない
- 再発行API自身の401が再発行処理を再帰的に呼ばない
- 再発行中に新しいAPIリクエストが発生しても、同じ更新結果を待てる
- タイマー更新と401検知が同時に起きても、更新APIが二重送信されない
ネットワーク速度も変えてみましょう。再発行APIが非常に速く成功すると、競合が見えにくくなります。逆に、再発行APIのレスポンスを遅延させると、待機キューへ複数のリクエストが入る状態を再現しやすくなります。
ブラウザの開発者ツールで確認するときは、通常APIの通信だけでなく、再発行APIの呼び出し回数と順番を見てください。再発行APIが1回だけで、元のリクエストが新しいトークンを使って再送されていることが、最初に見るべきポイントです。
ログを追加する場合は、トークンそのものを出力しないように注意してください。代わりに、次のような状態を記録すると、認証情報を漏らさずにプロセスを追跡できます。
- リクエスト識別子
- 401を受信した時刻
- 再発行処理を開始したか
- キューへ追加されたか
- 再発行が成功・失敗したか
- 再試行を行ったか
- 最終的なレスポンス結果
複数タブではAxios内の制御だけでは足りない
ここまで説明した isRefreshing や failedQueue は、基本的に同じJavaScript実行環境の中で機能します。
しかし、ブラウザで複数のタブを開いている場合、それぞれのタブは別の実行環境として動きます。タブAの isRefreshing が true になっても、タブBの変数まで同時に変わるわけではありません。
そのため、複数タブ間で同じリフレッシュトークンを共有している構成では、タブAとタブBがそれぞれ再発行APIを呼ぶ可能性があります。ここには、Axios単体のインターセプターだけでは解決できないプロジェクト固有の設計が必要です。
候補としては、ブラウザ間で状態を通知する仕組みや、共有ストレージの変更イベントを利用した調停があります。ただし、排他制御の有効期限、タブが閉じられた場合、通信が途中で失敗した場合などを考える必要があり、単純なフラグの共有では不十分です。
この記事の対象を同一タブ内の並行リクエストに限定するなら、まずはその範囲を明確にしておきましょう。そのうえで複数タブ対応が必要になった段階で、ブラウザ間の認証状態同期を別の問題として設計するのが現実的です。
認証更新処理は「通信の便利機能」ではなく状態遷移として設計する
Axiosのインターセプターは、API通信に共通処理を追加できる便利な仕組みです。しかし、トークン再発行まで含めると、扱っているのは単なるHTTP通信ではありません。
アプリケーションには、少なくとも次のような認証状態があります。
- 有効なアクセストークンがある
- アクセストークンが期限切れになった
- 再発行処理が進行中である
- 再発行に成功し、新しいトークンを保存した
- 再発行に失敗し、認証状態を破棄する
- 再試行しても認証できず、ユーザー操作が必要になる
これらの状態を曖昧にしたままコードだけを追加すると、401が返ったときの処理が場所によって変わってしまいます。結果として、あるAPIは再試行され、別のAPIは即座にログアウトし、さらに別のAPIは待機したままになる、といった不統一が起こります。
私が設計するときは、まず「再発行APIを呼べるのは誰か」「再発行中のリクエストはどこで待つか」「失敗時に誰へエラーを返すか」を決めます。その後でAxiosのリクエストインターセプターとレスポンスインターセプターへ責務を割り当てます。
この順番で考えると、フラグやキューが単なるおまじないではなく、認証状態を遷移させるための部品だと分かりやすくなります。
まとめ:再発行を一つに集約し、待機と終了を設計する
Axiosインターセプターによるトークン再発行で最も重要なのは、401を検知したすべてのリクエストが、個別に再発行処理を始めないことです。
同時に401が返ると、複数の再発行APIが競合し、特にリフレッシュトークンローテーションを採用したバックエンドでは、古いトークンの無効化によって意図しないログアウトが発生します。
実装では、次の役割を明確に分けてみましょう。
isRefreshingで再発行処理の担当者を一つに限定するfailedQueueで再発行を待つリクエストを保持する- 成功時は新しいトークンを保存して、待機中の処理を解放する
- 失敗時はキュー内の処理もすべて失敗として終了させる
config._retryで再試行回数を制限する- 再発行API自身を通常の401処理から除外する
- タイマー更新と401検知が同時に動く場合は、共通の更新Promiseなどで一つに束ねる
- 複数タブの競合は、Axios単体とは別の共有状態として設計する
まずは開発者ツールで、アクセストークン切れの状態から複数APIを同時に呼び出してみてください。再発行APIが何回送られ、どのリクエストが待機し、成功後に何本が再試行されるのかを確認すると、コードの見えにくい部分が具体的になります。
認証処理は、動いているときほど内部の競合が見えません。だからこそ、401が同時に返る状況を再現できるテストを用意し、再発行・待機・再試行・終了までのプロセスを一つずつ確認しておくことが、長く安定して運用できるフロントエンドへの近道になります。
Related reading: LaravelのAPI連携におけるRESTとGraphQLの長所と個人開発での最適解.