1
0

【メモ】Laravel API エラーフォーマットの上書きと注意点

Posted at

APIのエラーの上書き

概要

  • 今回の要件
    • POSTやPATCHなどの引数エラーなどなど、すべてのエラーはJSON形式でレスポンスをしたい
  • Laravelのデフォルトでは、HTML形式でエラーがレスポンスされてしまう。
  • エラーレスポンスも含めてレスポンスの形式を制御したい。

状況説明

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1364 Field 'unique_text' doesn't have a default value (Connection: mysql, SQL: insert into `card_formats` (`payment_name`, `updated_at`, `created_at`) values (rakuten, 2023-12-03 09:46:59, 2023-12-03 09:46:59)) in file /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 822

今回のエラーは、unique_textのデフォルトの値がないにもかかわらず、DBを作成しようとした結果発生したものである。
実際に作成するAPI要件としてunique_textがnullであることは認めていない必須のパラメータである。
これに対しての方法として一般的な対処としては、事前にバリデーションチェックをしっかりと行うことである。

事前生成済みとして話は進めていくが、App\Http\Requestsのstoreファイルにある

store.php
    public function rules(): array
    {
        return [
            'payment_name' => 'required|string|min:1|max:50',
            'unique_text' => 'required|string|min:1|max:255',
            'regexs' => 'required|string|min:7|max:255',
        ];
    }

のように、設定を行うことで、バリデーションチェックを行いエラーを回避することができる。

しかし、デフォルトの設定だと、400 Bad Requestは返らずにwelcomeページにリダイレクトしてしまう。

jsonで、エラー発生時間等も含めて返したい身としては使い勝手が悪い。

次から、jsonでエラーを返すようにする方法を取ろうと思う。

対策

Handler.php
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }

        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }

        return parent::render($request, $exception);
    }

    protected function getStatusCode($exception)
    {
        if (method_exists($exception, 'getStatusCode')) {
            return $exception->getStatusCode();
        }

        return 500; // デフォルトステータスコード
    }
}

Handler.phpにrender関数を追加することで、例外タイプがValidationExceptionであるときのレスポンスをラップして返すことで、指定した形式でレスポンスを返すことができるようになる。

response
{
    "success": false,
    "message": "バリデーションエラーです",
    "errors": {
        "regexs": [
            "The regexs field must be at least 7 characters."
        ]
    }
}

このような形で、レスポンスが返ってくる。

問題点

しかし、この状態だとほかのエラーもラップしているはずが、うまくラップできていないため、HTMLでレスポンスが返ってきてしまう場合がある。

ほかの例外に対処しているはずの部分
        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }
レスポンス
<!DOCTYPE html>
<html lang="en" class="auto">
<!--
Illuminate\Database\UniqueConstraintViolationException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry &#039;rakute&#039; for key &#039;card_formats.PRIMARY&#039; (Connection: mysql, SQL: insert into `card_formats` (`payment_name`, `unique_text`, `regexs`, `updated_at`, `created_at`) values (rakute, ksjfh, kkkkkokoko, 2023-12-04 02:23:59, 2023-12-04 02:23:59)) in file /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php on line 817

今回は、主キーである値が重複したため発生している。
例えこのような状態であったとしても、JSONの形式でレスポンスを返したい。

ほかの例外に対処している部分で、対処されていないことからコードを読み解くと、、

    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }

        // ここのif文が問題である!!!
        if ($request->expectsJson()) {
            // その他の例外に対するJSONレスポンス
            return response()->json([
                'success' => false,
                'message' => $exception->getMessage(),
            ], $this->getStatusCode($exception));
        }
        // さらに、ここですべてのif文に一致しなかった場合には親クラスのrenderに渡して処理を行わせている。
        return parent::render($request, $exception);
    }
原因
$request->expectsJson()
return parent::render($request, $exception);

どうやら、この部分はHTTPヘッダーのAcceptが[application/json]になっていない場合は、falseとなり、[application/json]の場合はtrueとなるようだ。

今回はPostmanのデフォルトの[/]の状態で行っていたため、後続の処理が行われず、HTMLの状態で帰ってきてしまった。

また、すべてのレスポンスをJSON形式にするのであれば、親のrenderに引き渡す必要がないため、削除すべきである。

最終的なHandler.phpの中身

Handler.php
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * The list of the inputs that are never flashed to the session on validation exceptions.
     *
     * @var array<int, string>
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    /**
     * Register the exception handling callbacks for the application.
     */
    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            //
        });
    }
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            // バリデーションエラーの場合のカスタムレスポンス
            return response()->json([
                'success' => false,
                'message' => 'バリデーションエラーです',
                'errors' => $exception->errors(),
            ], 400); // ステータスコードを400に設定
        }
        // その他の例外に対するJSONレスポンス
        return response()->json([
            'success' => false,
            'message' => $exception->getMessage(),
        ], $this->getStatusCode($exception));
    }

    protected function getStatusCode($exception)
    {
        if (method_exists($exception, 'getStatusCode')) {
            return $exception->getStatusCode();
        }

        return 500; // デフォルトステータスコード
    }
}

ただこの状態だと内部のエラー内容がすべて出てしまうため、基本的には「サーバー内エラーです。」程度の情報を返す方が良いだろう。

まとめ

意外と手間取ったが、Laravelがオブジェクト指向で作られているため、比較的修正が容易であった。最終的には、エラーに関しては量が増えやすいので、カスタムエラークラスをほかで作成して順次読み込む方が良いだろう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0