基于 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":"Resourcenotfound" }
理想情况下,它应该包含更多详细信息,以帮助API使用者处理错误。这是Facebook API如何返回错误的示例:
{ "error":{ "message":"Errorvalidatingaccesstoken:SessionhasexpiredonWednesday,14-Feb-1818:00:00PST.ThecurrenttimeisThursday,15-Feb-1813:46:35PST.", "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 获得以下响应:
RequestURL:http://q1.test/api/v1/offices RequestMethod:GET StatusCode:404NotFound { "message":"" }
至少 404 响应成功。其实可以做得更好,可以通过一些消息来解释错误。
为此你可以在 routes/api.PHP 的末尾指定 Route::fallback() 方法, 处理所有访问不存在路由的请求。
Route::fallback(function(){ returnresponse()->json([ 'message'=>'PageNotFound.Iferrorpersists,contactinfo@website.com'],404); });
结果还是相同的404响应,但现在出现了错误消息,提供了有关如何处理此错误的更多信息。
提示3.覆盖404 ModelNotFoundException
最常见就是找不到某些模型对象,通常由 Model :: findOrFail($ id) 抛出。以下是你的 API 会显示的典型消息:
{ "message":"Noqueryresultsformodel[App\\Office]2", "exception":"Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException", ... }
这是正确的,但向最终用户显示的消息不是很漂亮,因此,我的建议是重写对该特定异常的处理。
我们可以在 app/Exceptions/Handler.php (请记住该文件,我们将在以后多次返回它)中使用 render() 方法:
//Don'tforgetthisinthebeginningoffile useIlluminate\Database\Eloquent\ModelNotFoundException; //... publicfunctionrender($request,Exception$exception) { if($exceptioninstanceofModelNotFoundException){ returnresponse()->json([ 'message'=>'Entryfor'.str_replace('App\\','',$exception->getModel()).'notfound'],404); } returnparent::render($request,$exception); }
我们可以在这种方法中捕获任意数量的异常。在本例中,我们将返回相同的404代码,但可读性更高:
{ "message":"EntryforOfficenotfound" }
注意: 你有没有注意到一个有趣的方法?$exception->getModel() ?我们可以从 $Exception 对象中获得很多非常有用的信息,下面是 PhpStORM 自动完成的屏幕截图::
提示4:在验证中尽可能多捕获信息
开发人员一般不会考虑过多的验证规则,而是坚持使用诸如 required,date,emai 之类的简单规则。但是对于 API 而言,实际上错误的最典型原因是-消费者提交无效数据。
如果我们不花更多的精力来收集未通过验证的数据,那么 API 将通过后端验证,并抛出简单的 Server error,而没有任何详细信息(实际上原因是数据库查询错误)。
让我们看一下这个示例–我们在 Controller 中有一个 store() 方法:
publicfunctionstore(StoreOfficesRequest$request) { $office=Office::create($request->all()); return(newOfficeResource($office)) ->response() ->setStatusCode(201); }
我们的 FormRequest 文件 app/Http/Requests/StoreOfficesRequest.php 包含两个规则:
publicfunctionrules() { return[ 'city_id'=>'required|integer|exists:cities,id', 'address'=>'required' ]; }
如果我们遗漏了这两个参数并在其中传递空值,API 将返回一个相当易读的错误,带有 *422 * 状态码(此状态码默认是由于 Laravel 验证失败而产生):
{ "message":"Thegivendatawasinvalid.", "errors":{ "city_id":["Thecityidmustbeaninteger.","Thecityidfieldisrequired."], "address":["Theaddressfieldisrequired."] } }
它列出了所有字段错误,还提到了每个字段的所有错误,而不仅仅是捕获到的第一个错误。
现在,如果我们不指定那些验证规则并允许验证通过,以下是 API 返回:
{ "message":"ServerError" }
仅仅是服务器错误,没有其他有用的信息,什么是错误的,什么字段是缺失或不正确的。因此 API 使用者会懵逼。
所以我将在这里重复我的观点-请尝试在验证规则中捕获尽可能多的可能情况。检查字段是否存在、类型、最小-最大值、重复等
提示5 通常使用 Try-Catch 可以避免空的 500 服务器错误
继续上面的示例,使用 API 时,最糟糕的事情就是空错误。但是任何事情都会出错,尤其是在大型项目中,我们无法修复或预测随机错误。
但是,我们可以捕获他们!使用 try-catch PHP block。
想象一下这个控制器代码:
publicfunctionstore(StoreOfficesRequest$request) { $admin=User::find($request->email); $office=Office::create($request->all()+['admin_id'=>$admin->id]); (newUserService())->assignAdminToOffice($office); return(newOfficeResource($office)) ->response() ->setStatusCode(201); }->response()->setStatusCode(201);}
这是一个虚构的例子,也很常见。用电子邮件搜索用户,然后创建一条记录,对该记录进行操作。并且在任何步骤上,都可能发生错误。电子邮件可能为空,可能找不到管理员(或发现错误的管理员),服务方法可能会引发任何其他错误或异常等。
有很多处理和使用 try-catch 的方法,但是最流行的方法之一就是只捕获一个大的try-catch,然后对应是哪个异常类抛出的:
try{ $admin=User::find($request->email); $office=Office::create($request->all()+['admin_id'=>$admin->id]); (newUserService())->assignAdminToOffice($office); }catch(ModelNotFoundException$ex){//Usernotfound abort(422,'Invalidemail:administratornotfound'); }catch(Exception$ex){//Anythingthatwentwrong abort(500,'Couldnotcreateofficeorassignittoadministrator'); }
这样,我们可以随时调用 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,没有其他详细信息。但是我们可以捕获异常,并向消费者提供更多详细信息:
//在顶部 useGuzzleHttp\Exception\RequestException; //... try{ $client=new\GuzzleHttp\Client(); $response=$client->request('GET','https://api.github.com/repos/guzzle/guzzle123456'); }catch(RequestException$ex){ abort(404,'GithubRepositorynotfound'); }
提示6.1 创建自己的异常
我们甚至可以更进一步,创建我们自己的异常,特别是与一些第三方 API 错误相关的异常。
phpartisanmake:exceptionGithubAPIException
然后,我们新生成的文件 app/Exceptions/GithubAPIException.php将如下所示:
namespaceApp\Exceptions; useException; classGithubAPIExceptionextendsException { publicfunctionrender() { //... } }
我们甚至可以让它为空,但还是把它当作异常抛出。即使是异常 name,也可以帮助 API 用户避免将来的错误。所以我们这样做:
try{ $client=new\GuzzleHttp\Client(); $response=$client->request('GET','https://api.github.com/repos/guzzle/guzzle123456'); }catch(RequestException$ex){ thrownewGithubAPIException('GithubAPIfailedinOfficesController'); }
不仅如此-我们可以将错误处理移至 app / Exceptions / Handler.php 文件中(还记得上面吗?),如下所示:
publicfunctionrender($request,Exception$exception) { if($exceptioninstanceofModelNotFoundException){ returnresponse()->json(['error'=>'Entryfor'.str_replace('App\\','',$exception->getModel()).'notfound'],404); }elseif($exceptioninstanceofGithubAPIException){ returnresponse()->json(['error'=>$exception->getMessage()],500); }elseif($exceptioninstanceofRequestException){ returnresponse()->json(['error'=>'ExternalAPIcallfailed.'],500); } returnparent::render($request,$exception); }
最后的注意事项
以上就是我处理 API 错误的技巧,但这不是严格的规则。每个人都可以有自己的想法,如果你有自己的一些看法,可以在下面发表评论并进行讨论。
最后,除了错误处理之外,我想鼓励你做两件事:
为用户提供详细的 API 文档,请使用类似如下的包 API Generator;
返回 api 错误时,使用第三方服务 Bugsnag / Sentry / Rollbar。它们不是免费的,但是在调试时可以节省大量时间。
您可能感兴趣的文档:
Elasticsearch-PHP 中文文档
PHP 内核与原生扩展开发
Composer 中文文档
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~