📚

Laravel: APIの例外はJSONを返そう

2022/03/26に公開約3,000字

API へのリクエストで例外が発生したとき、Laravel デフォルトの HTML が返ってくるよ

JSON を受け取る気でいたのに、HTML がやってきた。
例外が発生したのね。わかるけどね。
HTML 渡されても扱いづらいのよね。

JSON で例外レスポンスを返すようにする

ドキュメントにも例があるけど、 App\Exceptions\Handler にカスタムレンダリングクロージャを登録することでオーバーライドできる。

Laravel のデフォルトのエラーページテンプレートには以下のステータスコードのものがあるので、これらの例外が発生した場合には代わりに JSON レスポンスを返すようにする。

  • 401: Unauthorized
  • 403: Forbidden
  • 404: Not Found
  • 419: Page Expired
  • 429: Too Many Requests
  • 500: Server Error
  • 503: Service Unavailable

実際のクロージャ

app/Exceptions/Handler.php の register メソッド内で、renderable メソッドを使って登録する。

パスが api/ で始まる場合のみ処理の対象とする。値を返さなければデフォルトの例外レンダーが使われるので、条件に当てはまらなければ元どおりの HTML が返るはず。

HttpException のステータスコードでスイッチしてレスポンスの内容を変える。

JSON の中身はエラーページテンプレートと同じ内容で、形式は RFC7807 風にしてみた。場合によってはもうちょっと詳しい情報を出してもいいかもしれない。

app/Exceptions/Handler.php
// :
use Symfony\Component\HttpKernel\Exception\HttpException;
// :

class Handler extends ExceptionHandler
{

    // :

    /**
     * Register the exception handling callbacks for the application.
     *
     * @return void
     */
    public function register()
    {

        // :

        $this->renderable(function (HttpException $e, $request) {
            if ($request->is('api/*')) {
                $title = '';
                $detail = '';

                switch ($e->getStatusCode()) {
                    case 401:
                        $title = __('Unauthorized');
                        $detail =  __('Unauthorized');
                        break;
                    case 403:
                        $title = __('Forbidden');
                        $detail = __($e->getMessage() ?: 'Forbidden');
                        break;
                    case 404:
                        $title = __('Not Found');
                        $detail = __('Not Found');
                        break;
                    case 419:
                        $title = __('Page Expired');
                        $detail = __('Page Expired');
                        break;
                    case 429:
                        $title = __('Too Many Requests');
                        $detail = __('Too Many Requests');
                        break;
                    case 500:
                        $title = __('Server Error');
                        $detail = __('Server Error');
                        break;
                    case 503:
                        $title = __('Service Unavailable');
                        $detail = __('Service Unavailable');
                        break;
                    default:
                        return;
                }

                return response()->json([
                    'title' => $title,
                    'status' => $e->getStatusCode(),
                    'detail' => $detail,
                ], $e->getStatusCode(), [
                    'Content-Type' => 'application/problem+json',
                ]);
            }
        });

        // :

    }
}

404 なんかは試しやすいですね。存在しない適当なパスを入力してみて、api/ から始まる場合とそうでない場合のレスポンスの違いを確認できます。

Discussion

ログインするとコメントできます