Laravel API 错误处理:当异常时,如何返回消息

原文链接:https://learnku.com/laravel/t/39571
讨论请前往专业的 Laravel 开发者论坛:https://learnku.com/Laravel

image

基于 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, 则将显示所有错误以及详细信息,包括类名称,数据库表等。

image

这是一个巨大的安全问题,因此在生产环境中,强烈建议将其设置为 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 自动完成的屏幕截图::

image

提示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/39571
讨论请前往专业的 Laravel 开发者论坛:https://learnku.com/Laravel

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 20,491评论 0 41
  • 来源:https://www.cnblogs.com/Qian123/p/5715402.html#_label0...
    Alex笔记阅读 375评论 0 1
  • ✅阅读25min ✅记账存钱 ✅专注一小时 ❌走路5000步 自省日作业 ❣️成功协助公司举办大型土地推介会 ①自...
    82阿岳北京2阅读 116评论 0 0
  • 今天有个小女孩对我说: “我昨天去看电影了” “嗯,看的什么电影呀?” “emmm...” “你和谁...
    沁早阅读 374评论 1 4
  • 今天是星期四。早上起床。妈妈早早的就把我叫起来啦!我八点就开始听课啦。我听完八点的课,我休息了一会儿,休息了一会儿...
    ee837014f764阅读 48评论 0 0