📑

[Laravel]CLIから内部APIにHttp::post()しようとして失敗した話

に公開

今回は、LaravelでCLIから自己エンドポイントにアクセスしようとした際の奮闘記です

やりたかったこと

テストデータ作成コマンドを実装しようとしていました。
任意の自己エンドポイントにアクセスして、データを作成するコマンドです。

最初の実装

以下はエンドポイントを叩く部分のメソッドの実装です。
指定したURLに対してpostするメソッドとなっています。

public static function post(string $table_name, string $url, array $data)
{
    $response = Http::withHeaders([
        'X-CSRF-TOKEN' => csrf_token(),
        'Cookie' => config('session.cookie') . '=' . session()->getId(), 
        ])
        ->post(url($url), $data);

    if ($response->getStatusCode() !== 200) {
         Log::error('error', ['status' => $response->getStatusCode());
    }
}

つまづき

バリデーションエラー時にステータス200で返ってくる

コマンド実行時に、ステータスは200で返ってきているのに意図したデータが作成されていないという事象が起こりました。
どこで失敗しているかが分からなかったため、バリデーション失敗時に必ず通るfailedValidation()でログ出力したところ、基本バリデーションで失敗していたことが分かりました。
バリデーション失敗時は元のURLにリダイレクトされるので、リダイレクト後のステータスコードとして200が返却されていたようでした。

解決策:
withoutRedirecting()を使って自動でリダイレクトしないようにしました。
リダイレクト前の状態でレスポンスが返ってくるようになったので、バリデーション失敗時のステータスコードは302になりました。

public static function post(string $table_name, string $url, array $data)
{
+    $response = Http::withoutRedirecting()
        ->withHeaders([
            'X-CSRF-TOKEN' => csrf_token(),
            'Cookie' => config('session.cookie') . '=' . session()->getId(), 
        ])
        ->post(url($url), $data);

    if ($response->getStatusCode() !== 200) {
        Log::error('error', ['status' => $response->getStatusCode());
    }
}

エラーメッセージが取得できない

バリデーションエラー時にステータスコードは302で返ってくるようにはなりましたが、エラーメッセージが取得できていませんでした。
バリデーションエラーメッセージはセッションに登録されているはず...と思いつつ、バリデーションエラー時の流れを改めてコードで追ってみました。

  • failedValidation()でValidationExceptionを投げてる
  • Exceptions\handlerでconvertValidationExceptionToResponse()が呼ばれている
  • withErrors()内でセッションのflashにエラーメッセージを設定している

コードからも、セッションにエラーメッセージが登録されていることが分かったので、Redis::get('laravel_hoge:' . session()->getId())でセッションの中身を確認したところ、エラーメッセージはちゃんと格納されていました。
しかしsession('errors')はnullで返却されます。
原因が分からなかったのでAIに聞いてみました。
どうやらCLIからの実行の場合、Http::post()だとLaravelアプリケーションのライフサイクルが実行されないため、\Illuminate\Session\Middleware\StartSessionが動かず、リクエスト元とリクエスト先でセッションデータの共有ができていないことが原因のようでした。

解決策:
Kernelでリクエストを送信するようにしました。
セッションからエラーメッセージを取得することができるようになりました。

public static function post(string $table_name, string $url, array $data)
{
+    $request = Request::create($url, 'POST', $data, [], [], [
+            'HTTP_X_CSRF_TOKEN' => csrf_token(),
+            'HTTP_COOKIE' => config('session.cookie') . '=' . session()->getId(), 
+    ]);
+    $kernel = app(Kernel::class);
+    $response = $kernel->handle($request);

+    $error_messages = ErrorHelper::getErrors(); // セッションからエラーメッセージを取得
    // ステータスコードが302以外の場合、エラーメッセージがある場合はエラーとして扱う(バリデーションエラー時も成功時も302で返ってくるため)
    if ($response->getStatusCode() !== 302 || $error_messages !== []) {
        $error_messages = ErrorHelper::getErrors();
        Log::error('error', ['status' => $response->getStatusCode(), 'error_messages' => $error_messages]);
    }
}

まとめ

自己エンドポイントにアクセスする際はHttp\Kernelを使ってリクエストする

Http::post()は外部 API へのリクエストに用いられ、一方、内部リクエストには Http\Kernelを使用するのが適切なようです。
前者は自己エンドポイントへのリクエストであっても、Laravelのライフサイクルから独立して処理されますが、後者はLaravelのライフサイクル全体を実行した上でリクエスト先の処理を行ってくれます。

補足:なぜRedis::get('laravel_hoge:' . session()->getId())にはエラーメッセージが格納されていたのに、session('errors')で取得できなかったのか?
セッションIDをヘッダーに設定した上でHttp::post()していたので、Redis上ではセッションは共有された状態になっています。
ただし、session()ヘルパー関数で取得できるのは、LaravelのSessionManagerによって管理されているセッションデータです。
Http::post()の場合、リクエスト元とリクエスト先でセッションは別物になるため、リクエスト先で更新されたセッションデータはリクエスト元のSessionManagerには反映されず、session('errors')で取得することはできなかったみたいでした。

補足:TestCaseのpost()もHttp\Kernelでリクエストしている
今回の実装はテストコードのテストデータ作成処理を参考にしていたのですが、そこで使われているMakesHttpRequestspost()も、Http\Kernelでリクエストしています。

補足:Tips

  • RedisのセッションIDのprefixがわからない場合
    • redis-cli KEYS "*"で確認できる

感想

いろいろと勉強になりました

参考

Discussion