はじめに

Laravelのメジャーアップデート(11から12)および、パッケージ(spatie/laravel-data v3からv4)のアップデート作業中、新旧の挙動を比較するためにGitブランチを行き来する「タイムトラベル検証」を行いました。

その際、「Gitとコンテナの実体のズレ」「例外の握りつぶし」「インフラの生存スコープ」という3つの罠にハマり、解決に時間を要しました。

そのため備忘録として、実際に遭遇したエラーの全貌と解決策に関してまとめます。

内容について、お気づきの点がございましたら、お教えいただけますと幸いです。

前提

  • Google Cloud上で動作する、PHPフレームワーク(Laravel)を使ったWebアプリケーション
  • 認証サービス
    • Firebase Authentication(Google)
  • トークン形式
    • JWT(JSON Web Token)

発端:アップデート前の挙動を過去に戻って確認

まずLaravel 11環境のベースとなるブランチから作業ブランチを派生させ、Laravel 12へのアップグレードに向けたコード修正を行いました。

一通り作業を終えて、「アップデート前のAPIレスポンス」と一致するかを確認するため、Gitのブランチを再び Laravel 11環境に切り替えました。

ブランチを変えたら composer install

Gitでブランチを切り替えても、vendor/ ディレクトリの中身(インストールされたパッケージの実体)は自動では過去に戻りません。
「コードは過去(v3)なのに、パッケージは未来(v4)」という不整合が起こることを防ぐため、ブランチ切り替え直後にコンテナ内で以下のコマンドを実行し、パッケージを同期させる必要があります。

// 当時の composer.lock の状態にパッケージをダウングレード
composer install

// 古いブランチの設定やクラス参照のキャッシュをリセットする
php artisan optimize:clear

エラー:謎の「401 Unauthorized」

環境の同期も行い、PostmanからAPIリクエストを送信しました。

しかし、返ってきたのは 401 Unauthorized(認証エラー)でした。

  • FirebaseのIDトークンの有効期限切れか? ➔ トークンを再発行してもNG。
  • キャッシュがおかしい? ➔ php artisan cache:clear を叩いてもNG。

動いていたはずの過去のコードが、頑なに認証を拒否する状態に突入しました。

探求:握りつぶされていた「真のエラー」

JWTのデコード処理のコードを確認したところ、どんなエラーが起きても、一律で401エラーを返すようになっていました。

// 修正前のコード
try {
$decoded = JWT::decode($idToken, $keySet);
} catch (\Exception $e) {
// どんなエラーが起きても、一律で401エラー
throw new AuthenticationErrorException();
}

ここで「認証エラーではなく、別のシステムエラーが起きていて、それが握りつぶされて401に変換されているだけなのでは?」と仮説を立てました。

そして、一時的に catch の中に dd($e->getMessage()); を仕込んで出力させたところ、以下の事実が判明しました。

  • 本当のエラー: ErrorException: Undefined array key 0
  • 発生箇所: CachedKeySet(Googleの公開鍵をキャッシュから取り出す処理)の内部

根本原因:消えないRedisキャッシュの罠

エラーの原因は「キャッシュアダプターの変更に伴う、Redis内のデータの不整合」でした。

アップデート作業の中で、キャッシュを扱うライブラリを RedisCachePool から RedisAdapter に変更していました。

  1. Laravel 12のブランチで、新しい形式のキャッシュがRedisに保存される。
  2. Laravel 11のブランチに戻る。
  3. Laravel 11のライブラリが、Laravel 12の新しい形式のキャッシュをRedisから読み込もうとする。
  4. シリアライズ形式が違うため配列が壊れており、[0] 番目の鍵が存在せずクラッシュ。

「なぜ php artisan cache:clear で直らなかったのか?」

今回エラーを起こしていたデータは、Laravelの管轄外でRedis内の別空間にデータを保存していたため、クリアコマンドをすり抜けて以前のデータが残っていました。

解決策:Redisキャッシュを指定して物理的に消去する

原因が「Redis内に残留した形式違いのJWT公開鍵キャッシュ」だと分かれば、Redisの中身を指定してJWT公開鍵キャッシュを物理的に消去します。

docker exec -it [redisコンテナ名] redis-cli -n [任意のDB番号] KEYS "プレフィックス*" | xargs docker exec -i [redisコンテナ名] redis-cli -n [任意のDB番号] DEL

これにより、ライブラリが安全にGoogleのサーバーから正しい形式で公開鍵を再取得・再キャッシュするようになり、無事に401の壁を突破。Laravel 11時代の正常なレスポンスと対面することができました!

まとめ

今回の検証から得た3つの教訓についてまとめます。

  1. ブランチを変えたら「コンテナ内で」パッケージ同期

    Gitはコードを過去に戻せますが、vendor/ の中身は戻してくれません。ブランチ切り替え直後は パッケージ更新(コンテナ内)と キャッシュクリアをセットで行う必要があります。

  2. 例外(Exception)の握りつぶしはデバッグの難易度を上げる

    システムエラーを安易にビジネスエラー(401など)に変換すると、エラー原因の追求が難しくなります。できる限り元のエラーを記録する設計にするのが良いと思いました。

  3. インフラの「状態」はGitでは巻き戻らない

    コードやパッケージを過去に戻しても、RedisやDBに書き込まれたデータは「最新」のままです。バージョンを行き来する際は、データストアのクリアもセットで考慮する必要があります。

同じように「バージョンを変えたら原因不明のエラーが出た」という方は、ぜひ「インフラの残留データ」を疑ってみてはいかがでしょうか。