Inertia.js×Laravelで、TokenMismatchExceptionのハンドリング
やりたいこと
画面を放置するなどで、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のドキュメントをみると、
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の中に定義する。
<?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