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ファイルにある
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でエラーを返すようにする方法を取ろうと思う。
対策
<?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であるときのレスポンスをラップして返すことで、指定した形式でレスポンスを返すことができるようになる。
{
"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 'rakute' for key 'card_formats.PRIMARY' (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の中身
<?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がオブジェクト指向で作られているため、比較的修正が容易であった。最終的には、エラーに関しては量が増えやすいので、カスタムエラークラスをほかで作成して順次読み込む方が良いだろう。