Laravel API 錯誤處理:當異常時,如何返回消息
原文鏈接:learnku.com/laravel/t/3…
討論請前往專業的 Laravel 開發者論壇:learnku.com/Laravel
基於 API 的項目開發越來越受歡迎,並且使用 Laravel 就能很容易實現。但是在針對如何處理各種異常的話題很少被提及。所以 API 的使用者們經常會抱怨除了收到 Server error ,很少有更多的錯誤信息。那么,我們該如何優雅的處理 API 錯誤讓其變得更具有可讀性呢?
目標:狀態碼 + 錯誤消息
對於 API 開發來講,正確的錯誤描述甚至比僅基於 Web 瀏覽器的項目更為重要。作為使用者,我們也可以通過瀏覽器消息提示清楚地了解錯誤以及該怎么解決。但對於 API 本身來說,它們是由軟件而非人員使用的,因此返回的結果應 readable by machines 。這意味着HTTP狀態代碼就必不可少。
API 給每個請求都會返回一個狀態碼,請求成功通常是 200,或者是以 2 開頭的其他狀態碼。
如果返回錯誤響應,則該響應不應包含2xx代碼,以下是最常見的錯誤代碼:
| 狀態碼 | 描述 | | 404 | 未找到(請求資源不存在) | | 401 | 未認證 (需要登錄) | | 403 | 沒有權限 | | 400 | 錯誤的請求(URL或參數不正確) | | 422 | 驗證失敗 | | 500 | 服務器錯誤 |
注意:返回響應時,如果沒有添加狀態碼,Laravel 會自動指定狀態碼,但並不能保證所指定的狀態碼正確。所以最好還是自己手動添加正確的狀態碼。
除此之外,我們還要考慮到 human-readable messages。因此,典型的響應應包含 HTTP 錯誤代碼和 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 的錯誤提示。
提示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:在驗證中盡可能多捕獲信息
開發人員一般不會考慮過多的驗證規則,而是堅持使用諸如 required,date,emai 之類的簡單規則。但是對於 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。它們不是免費的,但是在調試時可以節省大量時間。
原文鏈接:learnku.com/laravel/t/3…
討論請前往專業的 Laravel 開發者論壇:learnku.com/Laravel
