Restful API中的錯誤處理方法
簡介
隨著移動開發(fā)和前端開發(fā)的崛起,越來越多的 Web 后端應用都傾向于實現 Restful API。
Restful API 是一個簡單易用的前后端分離方案,它只需要對客戶端請求進行處理,然后返回結果即可, 無需考慮頁面渲染,一定程度上減輕了后端開發(fā)人員的負擔。
然而,正是由于 Restful API 不需要考慮頁面渲染,導致它不能在頁面上展示錯誤信息。
那就意著當出現錯誤的時候,它只能通過返回一個錯誤的響應,來告訴用戶和開發(fā)者相應的錯誤信息,提示他們接下來應該怎么辦。
本文將討論 Restful API 中的錯誤處理方案。
設計錯誤信息
當 Restful API 需要拋出錯誤的時候,我們要考慮的是:這個錯誤應該包含哪些信息。
我們先看看 Github, Google, Facebook, Twitter, Twilio 的錯誤信息是怎樣的。
Github (use http status)
{
"message": "Validation Failed",
"errors": [
{
"resource": "Issue",
"field": "title",
"code": "missing_field"
}
]
}
Google (use http status)
{
"error": {
"errors": [
{
"domain": "global",
"reason": "insufficientFilePermissions",
"message": "The user does not have sufficient permissions for file {fileId}."
}
],
"code": 403,
"message": "The user does not have sufficient permissions for file {fileId}."
}
}
Facebook (use http status)
{
"error": {
"message": "Message describing the error",
"type": "OAuthException",
"code": 190,
"error_subcode": 460,
"error_user_title": "A title",
"error_user_msg": "A message",
"fbtrace_id": "EJplcsCHuLu"
}
}
Twitter (use http status)
{
"errors": [
{
"message": "Sorry, that page does not exist",
"code": 34
}
]
}
Twilio (use http status)
{
"code": 21211,
"message": "The 'To' number 5551234567 is not a valid phone number.",
"more_info": "https://www.twilio.com/docs/errors/21211",
"status": 400
}
觀察這些結構可以發(fā)現它們都有一些共同的地方:
- 都利用了 Http 狀態(tài)碼
- 有些返回了業(yè)務錯誤碼
- 都提供了給用戶看的錯誤提示信息
- 有些提供了給開發(fā)者看的錯誤信息
Http 狀態(tài)碼
在 Restful API 中利用 Http 狀態(tài)碼來表明錯誤類型再合適不過了,因為 Http 狀態(tài)碼定義了很多抽象的錯誤類型。
雖然 Http 狀態(tài)碼定義了非常多的錯誤類型,但實際應用中,我們常用的狀態(tài)碼并不多,通常都是下面這幾方面:
- API 正常工作 (200, 201)
- 客戶端錯誤 (400, 401, 403, 404)
- 服務端錯誤 (500, 503)
業(yè)務錯誤碼
很多時候,我們根據業(yè)務類型來自定義錯誤碼。
這些業(yè)務錯誤碼與 Http 狀態(tài)碼并不重疊,這時候我們可以返回業(yè)務錯誤碼,用來提示用戶/開發(fā)者錯誤類型。
給用戶看的錯誤信息
當出現錯誤的時候,我們需要提示用戶如何處理這種情況,通常這種錯誤信息都是必須的。
可以看到上面幾個例子中都有返回給用戶看的錯誤信息。
給開發(fā)者看的錯誤信息
若我們的 API 需要開放給第三方開發(fā)者,那么我們就需要考慮返回一些給開發(fā)者看的錯誤信息。
設計錯誤類型
我們剛才提到過,可以利用 Http 狀態(tài)碼來為錯誤類型進行分類。
通常我們所說的分類通常是對客戶端錯誤進行分類, 即 4xx 類型的錯誤。
而這些錯誤類型中,我們最常用的是:
- 400 Bad Request
由于包含語法錯誤,當前請求無法被服務器理解。除非進行修改,否則客戶端不應該重復提交這個請求。
通常在請求參數不合法或格式錯誤的時候可以返回這個狀態(tài)碼。 - 401 Unauthorized
當前請求需要用戶驗證。
通常在沒有登錄的狀態(tài)下訪問一些受保護的 API 時會用到這個狀態(tài)碼。 - 403 Forbidden
服務器已經理解請求,但是拒絕執(zhí)行它。與401響應不同的是,身份驗證并不能提供任何幫助。
通常在沒有權限操作資源時(如修改/刪除一個不屬于該用戶的資源時)會用到這個狀態(tài)碼。 - 404 Not Found
請求失敗,請求所希望得到的資源未被在服務器上發(fā)現。
通常在找不到資源時返回這個狀態(tài)碼。
盡管我們可以通過 Http 狀態(tài)碼來表示錯誤的類型,
但在實際應用中,如果僅僅使用 Http 狀態(tài)碼的話,我們的代碼中就遍布 Http 狀態(tài)碼:
// Node.js
if (!res.body.title) {
res.statusCode = 400
}
if (!user) {
res.statusCode = 401
}
if (!post) {
res.statusCode = 404
}
上面的實現方式在小項目中還可以接受,當項目變大、需求變多的時候,維護起來就變得很麻煩了。
為了提高錯誤的可讀性和可維護性,我們需要對各種錯誤進行分類。
我個人習慣把錯誤分成以下幾種類型:
- 格式錯誤 (FORMAT_INVALID)
- 數據不存在 (DATA_NOT_FOUND)
- 數據已存在 (DATA_EXISTED)
- 數據無效 (DATA_INVALID)
- 登錄錯誤 (LOGIN_REQUIRED)
- 權限不足 (PERMISSION_DENIED)
錯誤分類之后,我們拋錯誤的時候就變得更加直觀了:
if (!res.body.title) {
throw new Error(ERROR.FORMAT_INVALID)
}
if (!user) {
throw new Error(ERROR.LOGIN_REQUIRED)
}
if (!post) {
throw new Error(ERROR.DATA_NOT_FOUND)
}
if (post.creator.id !== user.id) {
throw new Error(ERROR.PERMISSION_DENIED)
}
這種形式比上面的寫死狀態(tài)碼的方式方便很多,而且維護起來也更加簡單。
但有一個問題,就是不能根據錯誤類型來返回指定的錯誤信息。
自定義錯誤類型
要實現根據錯誤類型來返回指定的錯誤信息,我們可以通過自定義錯誤的方式來實現。
假設我們自定義錯誤的結構如下:
{
"type": "",
"code": 0,
"message": "",
"detail": ""
}
我們需要做到如下幾點:
- 根據錯誤類型來自動設置type, code, message
- detail 為可選項,用來描述該錯誤的具體原因
const ERROR = {
FORMAT_INVALID: 'FORMAT_INVALID',
DATA_NOT_FOUND: 'DATA_NOT_FOUND',
DATA_EXISTED: 'DATA_EXISTED',
DATA_INVALID: 'DATA_INVALID',
LOGIN_REQUIRED: 'LOGIN_REQUIRED',
PERMISSION_DENIED: 'PERMISSION_DENIED'
}
const ERROR_MAP = {
FORMAT_INVALID: {
code: 1,
message: 'The request format is invalid'
},
DATA_NOT_FOUND: {
code: 2,
message: 'The data is not found in database'
},
DATA_EXISTED: {
code: 3,
message: 'The data has exist in database'
},
DATA_INVALID: {
code: 4,
message: 'The data is invalid'
},
LOGIN_REQUIRED: {
code 5,
message: 'Please login first'
},
PERMISSION_DENIED: {
code: 6,
message: 'You have no permission to operate'
}
}
class CError extends Error {
constructor(type, detail) {
super()
Error.captureStackTrace(this, this.constructor)
let error = ERROR_MAP[type]
if (!error) {
error = {
code: 999,
message: 'Unknow error type'
}
}
this.name = 'CError'
this.type = error.code !== 999 ? type : 'UNDEFINED'
this.code = error.code
this.message = error.message
this.detail = detail
}
}
自定義好錯誤之后,我們調用起來就更加簡單了:
// in controller
if (!user) {
throw new CError(ERROR.LOGIN_REQUIRED, 'You should login first')
}
if (!req.body.title) {
throw new CError(ERROR.FORMAT_INVALID, 'Title is required')
}
if (!post) {
throw new CError(ERROR.DATA_NOT_FOUND, 'The post you required is not found')
}
最后,還剩下一個問題,根據錯誤類型來設置狀態(tài)碼,然后返回錯誤信息給客戶端。
捕獲錯誤信息
在 Controller 中拋出自定義錯誤后,我們需要捕獲該錯誤,才能返回給客戶端。
假設我們使用 koa 2 作為 web 框架來開發(fā) restful api,那么我們要做的是添加錯誤處理的中間件:
module.exports = async function errorHandler (ctx, next) {
try {
await next()
} catch (err) {
let status
switch (err.type) {
case ERROR.FORMAT_INVALID:
case ERROR.DATA_EXISTED:
case ERROR.DATA_INVALID:
status = 400
break
case ERROR.LOGIN_REQUIRED:
status = 401
case ERROR.PERMISSION_DENIED:
status = 403
case ERROR.DATA_NOT_FOUND:
status = 404
break
default:
status = 500
}
ctx.status = status
ctx.body = err
}
}
// in app.js
app.use(errorHandler)
app.use(router.routes())
通過這種方式,我們就能優(yōu)雅地處理 Restful API 中的錯誤信息了。
參考資料
- https://zh.wikipedia.org/zh-hans/HTTP%E7%8A%B6%E6%80%81%E7%A0%81
- https://www.loggly.com/blog/node-js-error-handling/
- http://blog.restcase.com/rest-api-error-codes-101/
- https://apigee.com/about/blg/technology/restful-api-design-what-about-errors
- http://stackoverflow.com/questions/942951/rest-api-error-return-good-practices
- http://goldbergyoni.com/checklist-best-practices-of-node-js-error-handling/
- http://blogs.mulesoft.com/dev/api-dev/api-best-practices-response-handling/
- https://developers.facebook.com/docs/graph-api/using-graph-api/#errors
- https://developers.google.com/drive/v3/web/handle-errors
- https://developer.github.com/v3/#client-errors
- https://dev.twitter.com/overview/api/response-codes
- https://www.twilio.com/docs/api/errors
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對腳本之家的支持。
相關文章
Springboot并發(fā)調優(yōu)之大事務和長連接
這篇文章主要介紹了Springboot并發(fā)調優(yōu)之大事務和長連接,重點分享長事務以及長連接導致的并發(fā)排查和優(yōu)化思路和示例,具有一定的參考價值,感興趣的可以了解一下2022-05-05
springboot2.x只需兩步快速整合log4j2的方法
這篇文章主要介紹了springboot2.x只需兩步快速整合log4j2的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-05-05
Java Map如何根據key取value以及不指定key取出所有的value
這篇文章主要介紹了Java Map如何根據key取value以及不指定key取出所有的value,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09

