🕌

Inertia.js×Laravelで、TokenMismatchExceptionのハンドリング

2023/11/30に公開

やりたいこと

画面を放置するなどで、CSRFトークンが切れて、CSRF保護のエラーになるときに、
セッション切れである旨のエラーを返したい。

現状

ブラウザの開発者ツールのapplicationタブでCookieを消した状態で、
router.postやuseFormのpost,putで、
POSTやPUTをすると、下記のように419が表示される。

前提

Laravel 10(※Laravel 11.1でのやりかたも追記しました。)
Inetia.js 1.0.0

解決策

App¥Exceptions\Handlerに、下記を記載

use Illuminate\Session\TokenMismatchException;

public function render($request, Throwable $e)
    {
        $response = parent::render($request, $e);
        if ($response->status() === 419 || $e instanceof TokenMismatchException) {
            return back(303)->withErrors(['画面を再読み込みしてください。']);
        return $response;
    }

こうすると、$request->validate()や、throw ValidationExceptionでエラーを返すのと同様に、フロントエンドにエラーを返せる。

ポイント①

withErrors(['画面を再読み込みしてください。'])

をつけてリダイレクトすること。

return back()->with([
    'errors' => '画面を再読み込みしてください。',
]);

だと、エラーになった。

return back();
だけすると、フロント側でそのPOSTやPUTの処理が成功したものとして扱われる。

ポイント②

back(303)

で、statusをデフォルトの302ではなく、303にして返す。
こうしないと、
POSTは問題なくても、PUTやDELETEにおいて、
405エラー(PUTやDELETEのルートがないぞ!)というようなエラーになる。
Inertia.jsのドキュメントをみると、
https://inertiajs.com/redirects

303 response code
When redirecting after a PUT, PATCH, or DELETE request, you must use a 303 response code, otherwise the subsequent request will not be treated as a GET request. A 303 redirect is very similar to a 302 redirect; however, the follow-up request is explicitly changed to a GET request.

If you're using one of our official server-side adapters, all redirects will automatically be converted to 303 redirects.

とあり、
要は、
303にして明示的にGETでリダイレクトするようにする必要がある模様。


Laravel 11の場合

Exceptionの定義方法が変わった

Laravel11からは、app/Exceptionsディレクトリがなくなる。

代わりに、bootstrap/app.phpで定義する。
withExceptionsの中に定義する。
https://laravel.com/docs/11.x/errors#custom-http-error-pages

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        ===中略===
    )
    ->withMiddleware(function (Middleware $middleware) {
        ===中略===
    })
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (HttpException $e) {
            //302のままだと、PUTやDELETEのリクエストがエラーになるため、303に変更
            if ($e->getStatusCode() === 419) {
                return back(303)->withErrors(['セッションが切れました。画面を再読み込みしてください。']);
            }
            return back(303)->withErrors(['エラーが発生しました。']);
        });
    })
    ->create();

ポイント①

Laravel10では、

if ($response->status() === 419 || $e instanceof TokenMismatchException)

で、TokenMismatchExceptionを拾っていたが、
Laravel11の、

->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (HttpException $e) {
            ~~~~~~~~~~
        });
    })

の書き方をすると、
$exceptions->render(function (TokenMismatchException $e)だと、render()内で定義したコールバック関数が動いてくれない。

という問題が発生。
どうやら、withExceptionsの段階では、渡されているExceptionに、TokenMismatchExceptionは、ないと判断されている模様。

render()の先を辿って、調べていくと、
vendor\laravel\framework\src\Illuminate\Foundation\Exceptions\Handler.php
で、

protected function prepareException(Throwable $e)
    {
        return match (true) {
            ===中略===
            $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),
            ===中略===
            default => $e,
        };
    }

という記載を発見。
どうやら、TokenMismatchExceptionは、HttpExceptionとして渡されている模様。
そのため、上述のように、

$exceptions->render(function (HttpException $e)

で、Exceptionを拾い、

if ($e->getStatusCode() === 419) {
    return back(303)->withErrors(['セッションが切れました。画面を再読み込みしてください。']);
}

return back(303)->withErrors(['エラーが発生しました。']);

のように、419の場合に絞って、セッション切れのメッセージを返している。
HttpExceptionは、上述のprepareExceptionを見る限り、
TokenMismatchExceptionの場合以外でも出現しうるようだったので、
一応、「エラーが発生しました。」として返すようにした。

とりあえず、これで419の場合のハンドリングはできるはず。

ただ、prepareExceptionはLaravel10でも同じであるものの、
Laravel10の場合、

if ($e instanceof TokenMismatchException)

としても、拾えた。いまいち腑に落ちない部分も・・・。。。

Discussion