Laravel API 錯誤處理:當異常時,如何返回消息

原文連接: https://learnku.com/laravel/t...
討論請前往專業的 Laravel 開發者論壇: https://learnku.com/Laravel

基於 API 的項目開發愈來愈受歡迎,而且使用 Laravel 就能很容易實現。可是在針對如何處理各類異常的話題不多被說起。因此 API 的使用者們常常會抱怨除了收到 Server error ,不多有更多的錯誤信息。那麼,咱們該如何優雅的處理 API 錯誤讓其變得更具備可讀性呢?php


目標:狀態碼 + 錯誤消息

對於 API 開發來說,正確的錯誤描述甚至比僅基於 Web 瀏覽器的項目更爲重要。做爲使用者,咱們也能夠經過瀏覽器消息提示清楚地瞭解錯誤以及該怎麼解決。但對於 API 自己來講,它們是由軟件而非人員使用的,所以返回的結果應 readable by machines 。這意味着HTTP狀態代碼就必不可少。laravel

API 給每一個請求都會返回一個狀態碼,請求成功一般是 200,或者是以 2 開頭的其餘狀態碼。git

若是返回錯誤響應,則該響應不該包含2xx代碼,如下是最多見的錯誤代碼:github

| 狀態碼 | 描述 |
| 404 | 未找到(請求資源不存在) |
| 401 | 未認證 (須要登陸) |
| 403 | 沒有權限 |
| 400 | 錯誤的請求(URL或參數不正確) |
| 422 | 驗證失敗 |
| 500 | 服務器錯誤 |web

注意:返回響應時,若是沒有添加狀態碼,Laravel 會自動指定狀態碼,但並不能保證所指定的狀態碼正確。因此最好仍是本身手動添加正確的狀態碼。數據庫

除此以外,咱們還要考慮到 human-readable messages。所以,典型的響應應包含 HTTP 錯誤代碼和 JSON 結果,以下所示:json

{
    "error": "Resource not found"
}

理想狀況下,它應該包含更多詳細信息,以幫助API使用者處理錯誤。這是Facebook API如何返回錯誤的示例:後端

{
  "error": {
    "message": "Error validating access token: Session has expired on Wednesday, 14-Feb-18 18:00:00 PST. The current time is Thursday, 15-Feb-18 13:46:35 PST.",
    "type": "OAuthException",
    "code": 190,
    "error_subcode": 463,
    "fbtrace_id": "H2il2t5bn4e"
  }
}

一般狀況下,錯誤內容就是須要在瀏覽器或移動端顯示的內容。所以最好根據須要提供儘量的細節。api

如今,讓咱們瞭解如何更好地改善 API 的錯誤提示。瀏覽器

提示1.即便在本地也要切換 APP_DEBUG=false

Laravel 的 .env 文件有一個重要的設置 APP_DEBUG ,它的值能夠爲  false or true

若是設置爲 true, 則將顯示全部錯誤以及詳細信息,包括類名稱,數據庫表等。

這是一個巨大的安全問題,所以在生產環境中,強烈建議將其設置爲 false

可是,我建議即便在本地也要針對 API 項目將其關閉,緣由以下。

關閉實際錯誤後,您將被迫像 API 使用者那樣思考,由於他們只會收到服務器錯誤(返回 Server error)而沒有更多的信息。換句話說,這時候你就須要考慮如何處理錯誤並提供合適的響應消息。


提示2:未處理的路由-回退方法

第一種狀況-若是有人調用不存在的 API 怎麼辦,有人甚至在 URL 中輸入錯誤的地址。默認狀況下,您從 API 得到如下響應:

Request URL: http://q1.test/api/v1/offices
Request Method: GET
Status Code: 404 Not Found
{
    "message": ""
}

至少 404 響應成功。其實能夠作得更好,能夠經過一些消息來解釋錯誤。

爲此你能夠在 routes/api.php 的末尾指定 Route::fallback() 方法, 處理全部訪問不存在路由的請求。

Route::fallback(function(){
    return response()->json([
        'message' => 'Page Not Found. If error persists, contact info@website.com'], 404);
});

結果仍是相同的404響應,但如今出現了錯誤消息,提供了有關如何處理此錯誤的更多信息。

提示3.覆蓋404 ModelNotFoundException

最多見就是找不到某些模型對象,一般由 Model :: findOrFail($ id) 拋出。如下是你的 API 會顯示的典型消息:

{
    "message": "No query results for model [App\\Office] 2",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
    ...
}

這是正確的,但向最終用戶顯示的消息不是很漂亮,所以,個人建議是重寫對該特定異常的處理。

咱們能夠在 app/Exceptions/Handler.php (請記住該文件,咱們將在之後屢次返回它)中使用 render() 方法:

// Don't forget this in the beginning of file
use Illuminate\Database\Eloquent\ModelNotFoundException;

// ...

public function render($request, Exception $exception)
{
    if ($exception instanceof ModelNotFoundException) {
        return response()->json([
            'error' => 'Entry for '.str_replace('App\\', '', $exception->getModel()).' not found'], 404);
    }

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

咱們能夠在這種方法中捕獲任意數量的異常。在本例中,咱們將返回相同的404代碼,但可讀性更高:

{
    "error": "Entry for Office not found"
}

注意: 你有沒有注意到一個有趣的方法?$exception->getModel() ?咱們能夠從 $Exception 對象中得到不少很是有用的信息,下面是 PhpStorm 自動完成的屏幕截圖::

提示4:在驗證中儘量多捕獲信息

開發人員通常不會考慮過多的驗證規則,而是堅持使用諸如 requireddateemai 之類的簡單規則。可是對於 API 而言,實際上錯誤的最典型緣由是-消費者提交無效數據。

若是咱們不花更多的精力來收集未經過驗證的數據,那麼 API 將經過後端驗證,並拋出簡單的 Server error,而沒有任何詳細信息(實際上緣由是數據庫查詢錯誤)。

讓咱們看一下這個示例–咱們在 Controller 中有一個 store() 方法:

public function store(StoreOfficesRequest $request)
{
    $office = Office::create($request->all());

    return (new OfficeResource($office))
        ->response()
        ->setStatusCode(201);
}

咱們的 FormRequest 文件 app/Http/Requests/StoreOfficesRequest.php 包含兩個規則:

public function rules()
{
    return [
        'city_id' => 'required|integer|exists:cities,id',
        'address' => 'required'
    ];
}

若是咱們遺漏了這兩個參數並在其中傳遞空值,API 將返回一個至關易讀的錯誤,帶有 422 狀態碼(此狀態碼默認是因爲 Laravel 驗證失敗而產生):

{
    "message": "The given data was invalid.",
    "errors": { 
        "city_id": ["The city id must be an integer.", "The city id field is required."],
        "address": ["The address field is required."]
    }
}

它列出了全部字段錯誤,還提到了每一個字段的全部錯誤,而不只僅是捕獲到的第一個錯誤。

如今,若是咱們不指定那些驗證規則並容許驗證經過,如下是 API 返回:

{
    "message": "Server Error"
}

僅僅是服務器錯誤,沒有其餘有用的信息,什麼是錯誤的,什麼字段是缺失或不正確的。所以 API 使用者會懵逼。

因此我將在這裏重複個人觀點-請嘗試在驗證規則中捕獲儘量多的可能狀況。檢查字段是否存在、類型、最小-最大值、重複等

提示5 一般使用 Try-Catch 能夠避免空的 500 服務器錯誤

繼續上面的示例,使用 API 時,最糟糕的事情就是空錯誤。可是任何事情都會出錯,尤爲是在大型項目中,咱們沒法修復或預測隨機錯誤。

可是,咱們能夠捕獲他們!使用 try-catch PHP block

想象一下這個控制器代碼:

public function store(StoreOfficesRequest $request)
{
    $admin = User::find($request->email);
    $office = Office::create($request->all() + ['admin_id' => $admin->id]);
    (new UserService())->assignAdminToOffice($office);

    return (new OfficeResource($office))
        ->response()
        ->setStatusCode(201);
}

這是一個虛構的例子,也很常見。用電子郵件搜索用戶,而後建立一條記錄,對該記錄進行操做。而且在任何步驟上,均可能發生錯誤。電子郵件可能爲空,可能找不到管理員(或發現錯誤的管理員),服務方法可能會引起任何其餘錯誤或異常等。

有不少處理和使用 try-catch 的方法,可是最流行的方法之一就是隻捕獲一個大的try-catch,而後對應是哪一個異常類拋出的:

try {
    $admin = User::find($request->email);
    $office = Office::create($request->all() + ['admin_id' => $admin->id]);
    (new UserService())->assignAdminToOffice($office);
} catch (ModelNotFoundException $ex) { // User not found
    abort(422, 'Invalid email: administrator not found');
} catch (Exception $ex) { // Anything that went wrong
    abort(500, 'Could not create office or assign it to administrator');
}

這樣,咱們能夠隨時調用 abort() 並添加所需的錯誤消息。若是咱們在每一個控制器(或其中的大多數控制器)中執行此操做,那麼咱們的 API 將返回與 Server error 相同的500,但包含更多可操做的錯誤消息。

提示6 經過捕獲異常來處理第三方 API 錯誤

現在,Web 項目使用大量外部 API,它們也可能會失敗。若是他們的 API 不錯,那麼他們將提供適當的異常和錯誤機制,所以咱們須要在應用程序中使用它。

例如,對某些 URL進行 Guzzle curl 請求並捕獲異常。

代碼很簡單:

$client = new \GuzzleHttp\Client();
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle123456');
// ... 用該響應作點什麼

您可能已經注意到,Github URL 無效,而且該存儲庫不存在。並且,若是咱們將代碼保持原樣,咱們的 API 將拋出 500 Server error,沒有其餘詳細信息。可是咱們能夠捕獲異常,並向消費者提供更多詳細信息:

// 在頂部
use GuzzleHttp\Exception\RequestException;

// ...

try {
    $client = new \GuzzleHttp\Client();
    $response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle123456');
} catch (RequestException $ex) {
    abort(404, 'Github Repository not found');
}

提示6.1 建立本身的異常

咱們甚至能夠更進一步,建立咱們本身的異常,特別是與一些第三方 API 錯誤相關的異常。

php artisan make:exception GithubAPIException

而後,咱們新生成的文件 app/Exceptions/GithubAPIException.php將以下所示:

namespace App\Exceptions;

use Exception;

class GithubAPIException extends Exception
{

    public function render()
    {
        // ...
    }

}

咱們甚至可讓它爲空,但仍是把它看成異常拋出。即便是異常 name,也能夠幫助 API 用戶避免未來的錯誤。因此咱們這樣作:

try {
    $client = new \GuzzleHttp\Client();
    $response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle123456');
} catch (RequestException $ex) {
    throw new GithubAPIException('Github API failed in Offices Controller');
}

不只如此-咱們能夠將錯誤處理移至 app / Exceptions / Handler.php 文件中(還記得上面嗎?),以下所示:

public function render($request, Exception $exception)
{
    if ($exception instanceof ModelNotFoundException) {
        return response()->json(['error' => 'Entry for '.str_replace('App\\', '', $exception->getModel()).' not found'], 404);
    } else if ($exception instanceof GithubAPIException) {
        return response()->json(['error' => $exception->getMessage()], 500);
    } else if ($exception instanceof RequestException) {
        return response()->json(['error' => 'External API call failed.'], 500);
    }

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

最後的注意事項

以上就是我處理 API 錯誤的技巧,但這不是嚴格的規則。每一個人均可以有本身的想法,若是你有本身的一些見解,能夠在下面發表評論並進行討論。

最後,除了錯誤處理以外,我想鼓勵你作兩件事:

  • 爲用戶提供詳細的 API 文檔,請使用相似以下的包 API Generator;
  • 返回 api 錯誤時,使用第三方服務 Bugsnag / Sentry / Rollbar。它們不是免費的,可是在調試時能夠節省大量時間。
原文連接: https://learnku.com/laravel/t...
討論請前往專業的 Laravel 開發者論壇: https://learnku.com/Laravel