Laravel 打造適合 API 的異常處理響應格式

Mr-houzi發表於2020-06-12

Laravel全域性捕獲異常後,會把異常轉為相應的資料格式返回給使用者。如果想要規定的資料格式相應,那我們只需重寫異常捕獲後的處理方法即可。

異常處理流程

Illuminate\Foundation\Exception\Handler 中的 render 方法用來將異常轉化為響應。

public function render($request, Exception $e)
{
    if (method_exists($e, 'render') && $response = $e->render($request)) {
        return Router::toResponse($request, $response);
    } elseif ($e instanceof Responsable) {
        return $e->toResponse($request);
    }

    $e = $this->prepareException($e);

    if ($e instanceof HttpResponseException) {
        return $e->getResponse();
    } elseif ($e instanceof AuthenticationException) {
        return $this->unauthenticated($request, $e);
    } elseif ($e instanceof ValidationException) {
        return $this->convertValidationExceptionToResponse($e, $request);
    }

    return $request->expectsJson()
                    ? $this->prepareJsonResponse($request, $e)
                    : $this->prepareResponse($request, $e);
}

render() 中又呼叫了 prepareException() 對部分異常進行預處理,但並未執行轉化為響應的操作。

ModelNotFoundException 一般在模型查詢不到丟擲,prepareException() 中它被轉為 Symfony 包中NotFoundHttpException,預設狀態碼404;

AuthorizationException 在 Policy 許可權未透過時丟擲,prepareException() 中它被轉為 Symfony 包中 AccessDeniedHttpException,預設狀態碼403;

TokenMismatchException 在 CSRF 驗證未透過時丟擲,prepareException() 中它被轉為 Symfony 包中 HttpException,給定狀態碼419;

其他異常直接返回。

protected function prepareException(Exception $e)
{
    if ($e instanceof ModelNotFoundException) {
        $e = new NotFoundHttpException($e->getMessage(), $e);
    } elseif ($e instanceof AuthorizationException) {
        $e = new AccessDeniedHttpException($e->getMessage(), $e);
    } elseif ($e instanceof TokenMismatchException) {
        $e = new HttpException(419, $e->getMessage(), $e);
    }
    return $e;
}

在回到 render() ,預處理異常之後,又分別對 HttpResponseException、AuthenticationException 和 ValidationException 單獨處理,並轉為響應返回。

除此以外的異常,都在 prepareJsonResponse() 或 prepareResponse() 處理 ,expectsJson() 用來判斷返回 json 響應還是普通響應。

修改異常響應格式

瞭解了異常處理流程,接下來就處理異常響應格式。

修改登入認證異常格式

由上文可知,AuthenticationException 被捕獲後,呼叫 unauthenticated() 來處理。

protected function unauthenticated($request, AuthenticationException $exception)
{
    return $request->expectsJson()
                ? response()->json(['message' => $exception->getMessage()], 401)
                : redirect()->guest($exception->redirectTo() ?? route('login'));
}

在 app\Exceptions\Handler.php 中重寫 unauthenticated() 使其返回我們想要的資料格式。

protected function unauthenticated($request, AuthenticationException $exception)
{
    return $request->expectsJson()
        ? response()->json([
            'code' => 0,
            'data' => $exception->getMessage(),
        ], 401)
        : redirect()->guest($exception->redirectTo() ?? route('login'));
}

修改驗證異常格式

同樣由上文可知,ValidationException 被捕獲後交由 convertValidationExceptionToResponse() 處理,進入此方法後我們需要繼續追蹤,若是需要 json 響應,最終交由 invalidJson() 處理。

protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{
    if ($e->response) {
        return $e->response;
    }

    return $request->expectsJson()
                ? $this->invalidJson($request, $e)
                : $this->invalid($request, $e);
}
protected function invalidJson($request, ValidationException $exception)
{
    return response()->json([
        'message' => $exception->getMessage(),
        'errors' => $exception->errors(),
    ], $exception->status);
}

我們繼續在 app\Exceptions\Handler.php 重寫 invalidJson() 即可自定義返回格式。

protected function invalidJson($request, ValidationException $exception)
{
    return response()->json([
        'code' => 0,
        'data' => $exception->errors(),
    ], $exception->status);
}

修改其他異常格式

其他異常是呼叫 prepareJsonResponse() 來處理,此方法又呼叫 convertExceptionToArray() 來處理響應格式。

protected function prepareJsonResponse($request, Exception $e)
{
    return new JsonResponse(
        $this->convertExceptionToArray($e),
        $this->isHttpException($e) ? $e->getStatusCode() : 500,
        $this->isHttpException($e) ? $e->getHeaders() : [],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
    );
}
protected function convertExceptionToArray(Exception $e)
{
    return config('app.debug') ? [
        'message' => $e->getMessage(),
        'exception' => get_class($e),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => collect($e->getTrace())->map(function ($trace) {
            return Arr::except($trace, ['args']);
        })->all(),
    ] : [
        'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
    ];
}

在 app\Exceptions\Handler.php 中重寫 convertExceptionToArray() 來自定義其他異常響應格式。

protected function convertExceptionToArray(Exception $e)
{
    return config('app.debug') ? [
        'code' => 0,
        'data' => $e->getMessage(),
        'exception' => get_class($e),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => collect($e->getTrace())->map(function ($trace) {
            return Arr::except($trace, ['args']);
        })->all(),
    ] : [
        'code' => 0,
        'data' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
    ];
}

強制 json 響應

程式碼中多次出現了 expectsJson() ,此方法是用來判斷返回 json 響應還是普通響應。

public function expectsJson()
{
    return ($this->ajax() && ! $this->pjax() && $this->acceptsAnyContentType()) || $this->wantsJson();
}

以下兩種條件下,會返回json響應。

  1. 非XML請求、非pjax並且 Headers 中 Accept 設定為接收所有格式響應;

  2. Headers Accept 設定為 /json、+json。如:Accept:application/json。

除此之外的情況,將不會響應json。我們可以利用中介軟體強制追加 Accept:application/json,使異常響應時都返回json。(參考教程 L03 6.0 中提到的方法)

建立中介軟體 AcceptHeader

<?php

namespace App\Http\Middleware;

use Closure;

class AcceptHeader
{
    public function handle($request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');
        return $next($request);
    }
}

在 app/Http/Kernel.php 中,將中介軟體加入路由組即可。

protected $middlewareGroups = [
    'web' => [
        .
        .
        .
    'api' => [
        \App\Http\Middleware\AcceptHeader::class,
        'throttle:60,1',
        'bindings',
    ],
];

大功告成。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章