Обработка исключений в Laravel при AJAX запросе

Обработка исключений в Laravel при AJAX запросе

Во время выбрасывания исключений Laravel проверяет, есть ли в классе исключения метод render(), если да, то он использует метод этого исключения для отображения результата. Если вы не хотите полагаться на глобальную систему отлова исключений Laravel, то можете вернуть ответ в JSON напрямую из контроллера.

Laravel пытается преобразовать исключения в читаемый формат в зависимости от ожидаемого от клиента формата ответа, будь то HTML или JSON, сначала он преобразует различные форматы исключений в простое исключение типа HttpException:

if ($e instanceof ModelNotFoundException) {
    $e = new NotFoundHttpException($e->getMessage(), $e);
} elseif ($e instanceof AuthorizationException) {
    $e = new HttpException(403, $e->getMessage());
} elseif ($e instanceof TokenMismatchException) {
    $e = new HttpException(419, $e->getMessage());
}

И только затем он обрабатывает некоторые исключения особым образом.

Illuminate\Http\Exceptions\HttpResponseException - это особое исключение, встроенное в Laravel. Особенность этого исключения заключается в том, что оно уже содержит шаблон ответа для клиента, поэтому Laravel просто возвращает ответ из этого исключения.

Рендеринг исключений аутентификации

Исключение аутентификации Illuminate\Auth\AuthenticationException обрабатывается с использованием метода unauthenticated(), который по умолчанию редиректит пользователя на URL-адрес /login для повторной аутентификации. Но, в случае, если клиент присылает заголовок Accept: application/json то будет возвращён объект в формате JSON и 401 статусом:

{"message" : "Unauthenticated."}

Конечно, Laravel не считался бы гибким фреймворком, если бы это поведение нельзя было изменить.

Рендеринг исключений при валидации

В случае, когда выбрасывается исключение типа Illuminate\Validation\ValidationException, фреймворк перенаправляет пользователя на URL-адрес с которого пользователь делал запрос, передавая сессией список из всех ошибок валидации, и это дает вам возможность проверить, содержит ли переменная $errors любые ошибки, которые вы можете вывести на экране:

@if (count($errors) > 0)
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

Если клиентом был запрошен формат представления - JSON, то Laravel возвращает объект с HTTP-кодом 422, который выглядит следующим образом:

{
  "message": "The given data failed to pass validation.",
  "errors": {
    "email": [
        "The email field is required.",
        "The field under validation must be formatted as an e-mail address."
    ]
  }
}

Рендеринг остальных исключений

На этом этапе все специальные исключения либо преобразуются в простое исключение HttpException, либо обрабатываются особым образом. Для остальных же исключений, Laravel сначала проверяет, какой из форматов ответа ожидает клиент: HTML или JSON, а затем, исходя из этого, действует соответствующим образом.

Как Laravel определяет какой формат ответа клиент ожидает получить?

Laravel использует метод expectedJson() класса Illuminate\Http\Request. Этот метод проверяет наличие заголовка X-Requested-With и содержит ли он значение XMLHttpRequest. Подобный заголовок устанавливается большинством структур JavaScript при AJAX запросе, и Laravel использует его, чтобы предположить, что запрос действительно является AJAX-запросом. Однако, помимо этого, Laravel также проверяет заголовок X-PJAX в запросе и просто возвращает false, если он присутствует, поскольку этот заголовок указывает на то, что ответ должен быть не в формате JSON, а в виде обычного HTML-кода.

Наконец, он проверяет заголовок Accept, в котором проверяет, ожидает ли клиент JSON в ответ.

Итак, если клиент ожидает JSON-ответ, как Laravel преобразует исключение в JSON?

Если в конфигах фреймворка, параметру app.debug задано значение true, Laravel преобразует исключение в формат JSON со следующей структурой:

{
    "message": "...",
    "file": "...",
    "line": ...,
    "trace": "..."
}

Это очень помогает во время разработки, так как это дает больше информации разработчику, для понимания того, что пошло не так во время обработки запроса. Однако, очевидно, что параметру app.debug не должно быть установлено значение true в продакшене, поскольку подобное сообщение может предоставить конфиденциальную информацию сторонним лицам. В этом случае Laravel, проверяется, является ли выброшенное исключение типом HttpException, и возвращает сообщение об ошибке в виде следующей структуры JSON:

{
    "message": "..."
}

Однако, если исключение не является подтипом HTTP, или исключения с кодом 500, Laravel просто возвращает сообщение "Server Error":

{
    "message": "Server Error"
}

Если вы доверяете пользователям своего API, то создайте кастомное исключение типа HTTP, и они смогут получать сообщения ошибок. Иначе же Laravel возьмёт работу по защите ваших данных, скрыв фактическое сообщение исключения, и покажет только ошибку "Server Error".

Что происходит, когда ожидается ответ в формате HTML?

Сначала Laravel проверяет, есть ли у вас какие-либо представления в вашем каталоге resources/views/errors с именем кода состояния статуса ошибки, например, 404.blade.php, 500.blade.php, .... Если представление существует, то Laravel отображает его в браузере клиента.

Если такого представления не было найдено, Laravel будет использовать обработчик исключений по умолчанию, коим является обработчик от Symfony. Он отображает красивое представление с детальной информацией об исключении в случае, если app.debug включен, или "Whoops, looks like something went wrong.", если режим отладки отключён.

Изменение стандартного формата отображения ошибок

Иногда возникает необходимость в изменении формата отображения ошибок валидации при AJAX-запросе. По умолчанию Laravel показывает ошибки в формате:

{
    "message":"The given data was invalid.",
    "errors": {
        "name": ["The name field is required."],
        "email": ["The email must be a valid email address.", "The email must be at least 4 characters."]
    }
}

Если вам нужно кастомизировать этот ответ, и сделать какой-то нестандартный вывод, к примеру, вывод только списка сообщений, без группировки их под соответствующими ключами, то нужно изменить файл app/Exceptions/Handler.php:

// проверяем, что это AJAX запрос, или ответ требуется вернуть в формате JSON
if(($request->ajax() && !$request->pjax()) || $request->wantsJson()) {
    // проверяем, что исключение является типом ошибки валидации
    if($exception instanceof ValidationException) {
        return new JsonResponse([
            'success' => false,
            'errors' => \Illuminate\Support\Arr::collapse($exception->errors()),
            'message' => $exception->getMessage()
        ], 422);
    }

    return new JsonResponse([
        'success' => false,
        'message' => $exception->getMessage()
    ], 422);
}

В результате этой модификации, ответ будет выглядеть:

{
    "success":false,
    "errors": [
        "The name field is required.",
        "The email must be a valid email address.",
        "The email must be at least 4 characters."
    ],
    "message":"The given data was invalid."
}

Аналогично этому вы можете кастомизировать индивидуально любой тип исключения. Или же, можете написать общий обработчик для всех исключений. Типа того, что был добавлен несколько ниже обработки исключений валидации:

return new JsonResponse([
    'success' => false,
    'message' => $exception->getMessage()
], 422);

Резюме

В этой статье я подробно рассказал, как работает отлов и обработка исключений в Laravel. Был показан весь их жизненный цикл, и объяснена работа каждого из них. Так же, был продемонстрирован пример того, как отобразить кастомный ответ в JSON при ошибках валидации, и различных HTTP-статусах. Эта статья появилась после написания первой статьи о создании кастомных страниц при исключениях, где были рассмотрены примеры отлова исключений и соответствующех их индивидуальной обработки. После прочтения этих статей вы можете сказать, что знаете об обработке исключений в Laravel почти всё.