Restful API中的錯(cuò)誤處理方法
簡(jiǎn)介
隨著移動(dòng)開(kāi)發(fā)和前端開(kāi)發(fā)的崛起,越來(lái)越多的 Web 后端應(yīng)用都傾向于實(shí)現(xiàn) Restful API。
Restful API 是一個(gè)簡(jiǎn)單易用的前后端分離方案,它只需要對(duì)客戶端請(qǐng)求進(jìn)行處理,然后返回結(jié)果即可, 無(wú)需考慮頁(yè)面渲染,一定程度上減輕了后端開(kāi)發(fā)人員的負(fù)擔(dān)。
然而,正是由于 Restful API 不需要考慮頁(yè)面渲染,導(dǎo)致它不能在頁(yè)面上展示錯(cuò)誤信息。
那就意著當(dāng)出現(xiàn)錯(cuò)誤的時(shí)候,它只能通過(guò)返回一個(gè)錯(cuò)誤的響應(yīng),來(lái)告訴用戶和開(kāi)發(fā)者相應(yīng)的錯(cuò)誤信息,提示他們接下來(lái)應(yīng)該怎么辦。
本文將討論 Restful API 中的錯(cuò)誤處理方案。
設(shè)計(jì)錯(cuò)誤信息
當(dāng) Restful API 需要拋出錯(cuò)誤的時(shí)候,我們要考慮的是:這個(gè)錯(cuò)誤應(yīng)該包含哪些信息。
我們先看看 Github, Google, Facebook, Twitter, Twilio 的錯(cuò)誤信息是怎樣的。
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 }
觀察這些結(jié)構(gòu)可以發(fā)現(xiàn)它們都有一些共同的地方:
- 都利用了 Http 狀態(tài)碼
- 有些返回了業(yè)務(wù)錯(cuò)誤碼
- 都提供了給用戶看的錯(cuò)誤提示信息
- 有些提供了給開(kāi)發(fā)者看的錯(cuò)誤信息
Http 狀態(tài)碼
在 Restful API 中利用 Http 狀態(tài)碼來(lái)表明錯(cuò)誤類型再合適不過(guò)了,因?yàn)?Http 狀態(tài)碼定義了很多抽象的錯(cuò)誤類型。
雖然 Http 狀態(tài)碼定義了非常多的錯(cuò)誤類型,但實(shí)際應(yīng)用中,我們常用的狀態(tài)碼并不多,通常都是下面這幾方面:
- API 正常工作 (200, 201)
- 客戶端錯(cuò)誤 (400, 401, 403, 404)
- 服務(wù)端錯(cuò)誤 (500, 503)
業(yè)務(wù)錯(cuò)誤碼
很多時(shí)候,我們根據(jù)業(yè)務(wù)類型來(lái)自定義錯(cuò)誤碼。
這些業(yè)務(wù)錯(cuò)誤碼與 Http 狀態(tài)碼并不重疊,這時(shí)候我們可以返回業(yè)務(wù)錯(cuò)誤碼,用來(lái)提示用戶/開(kāi)發(fā)者錯(cuò)誤類型。
給用戶看的錯(cuò)誤信息
當(dāng)出現(xiàn)錯(cuò)誤的時(shí)候,我們需要提示用戶如何處理這種情況,通常這種錯(cuò)誤信息都是必須的。
可以看到上面幾個(gè)例子中都有返回給用戶看的錯(cuò)誤信息。
給開(kāi)發(fā)者看的錯(cuò)誤信息
若我們的 API 需要開(kāi)放給第三方開(kāi)發(fā)者,那么我們就需要考慮返回一些給開(kāi)發(fā)者看的錯(cuò)誤信息。
設(shè)計(jì)錯(cuò)誤類型
我們剛才提到過(guò),可以利用 Http 狀態(tài)碼來(lái)為錯(cuò)誤類型進(jìn)行分類。
通常我們所說(shuō)的分類通常是對(duì)客戶端錯(cuò)誤進(jìn)行分類, 即 4xx 類型的錯(cuò)誤。
而這些錯(cuò)誤類型中,我們最常用的是:
- 400 Bad Request
由于包含語(yǔ)法錯(cuò)誤,當(dāng)前請(qǐng)求無(wú)法被服務(wù)器理解。除非進(jìn)行修改,否則客戶端不應(yīng)該重復(fù)提交這個(gè)請(qǐng)求。
通常在請(qǐng)求參數(shù)不合法或格式錯(cuò)誤的時(shí)候可以返回這個(gè)狀態(tài)碼。 - 401 Unauthorized
當(dāng)前請(qǐng)求需要用戶驗(yàn)證。
通常在沒(méi)有登錄的狀態(tài)下訪問(wèn)一些受保護(hù)的 API 時(shí)會(huì)用到這個(gè)狀態(tài)碼。 - 403 Forbidden
服務(wù)器已經(jīng)理解請(qǐng)求,但是拒絕執(zhí)行它。與401響應(yīng)不同的是,身份驗(yàn)證并不能提供任何幫助。
通常在沒(méi)有權(quán)限操作資源時(shí)(如修改/刪除一個(gè)不屬于該用戶的資源時(shí))會(huì)用到這個(gè)狀態(tài)碼。 - 404 Not Found
請(qǐng)求失敗,請(qǐng)求所希望得到的資源未被在服務(wù)器上發(fā)現(xiàn)。
通常在找不到資源時(shí)返回這個(gè)狀態(tài)碼。
盡管我們可以通過(guò) Http 狀態(tài)碼來(lái)表示錯(cuò)誤的類型,
但在實(shí)際應(yīng)用中,如果僅僅使用 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 }
上面的實(shí)現(xiàn)方式在小項(xiàng)目中還可以接受,當(dāng)項(xiàng)目變大、需求變多的時(shí)候,維護(hù)起來(lái)就變得很麻煩了。
為了提高錯(cuò)誤的可讀性和可維護(hù)性,我們需要對(duì)各種錯(cuò)誤進(jìn)行分類。
我個(gè)人習(xí)慣把錯(cuò)誤分成以下幾種類型:
- 格式錯(cuò)誤 (FORMAT_INVALID)
- 數(shù)據(jù)不存在 (DATA_NOT_FOUND)
- 數(shù)據(jù)已存在 (DATA_EXISTED)
- 數(shù)據(jù)無(wú)效 (DATA_INVALID)
- 登錄錯(cuò)誤 (LOGIN_REQUIRED)
- 權(quán)限不足 (PERMISSION_DENIED)
錯(cuò)誤分類之后,我們拋錯(cuò)誤的時(shí)候就變得更加直觀了:
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) }
這種形式比上面的寫(xiě)死狀態(tài)碼的方式方便很多,而且維護(hù)起來(lái)也更加簡(jiǎn)單。
但有一個(gè)問(wèn)題,就是不能根據(jù)錯(cuò)誤類型來(lái)返回指定的錯(cuò)誤信息。
自定義錯(cuò)誤類型
要實(shí)現(xiàn)根據(jù)錯(cuò)誤類型來(lái)返回指定的錯(cuò)誤信息,我們可以通過(guò)自定義錯(cuò)誤的方式來(lái)實(shí)現(xiàn)。
假設(shè)我們自定義錯(cuò)誤的結(jié)構(gòu)如下:
{ "type": "", "code": 0, "message": "", "detail": "" }
我們需要做到如下幾點(diǎn):
- 根據(jù)錯(cuò)誤類型來(lái)自動(dòng)設(shè)置type, code, message
- detail 為可選項(xiàng),用來(lái)描述該錯(cuò)誤的具體原因
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 } }
自定義好錯(cuò)誤之后,我們調(diào)用起來(lái)就更加簡(jiǎn)單了:
// 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') }
最后,還剩下一個(gè)問(wèn)題,根據(jù)錯(cuò)誤類型來(lái)設(shè)置狀態(tài)碼,然后返回錯(cuò)誤信息給客戶端。
捕獲錯(cuò)誤信息
在 Controller 中拋出自定義錯(cuò)誤后,我們需要捕獲該錯(cuò)誤,才能返回給客戶端。
假設(shè)我們使用 koa 2 作為 web 框架來(lái)開(kāi)發(fā) restful api,那么我們要做的是添加錯(cuò)誤處理的中間件:
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())
通過(guò)這種方式,我們就能優(yōu)雅地處理 Restful API 中的錯(cuò)誤信息了。
參考資料
- 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
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Springboot并發(fā)調(diào)優(yōu)之大事務(wù)和長(zhǎng)連接
這篇文章主要介紹了Springboot并發(fā)調(diào)優(yōu)之大事務(wù)和長(zhǎng)連接,重點(diǎn)分享長(zhǎng)事務(wù)以及長(zhǎng)連接導(dǎo)致的并發(fā)排查和優(yōu)化思路和示例,具有一定的參考價(jià)值,感興趣的可以了解一下2022-05-05springboot2.x只需兩步快速整合log4j2的方法
這篇文章主要介紹了springboot2.x只需兩步快速整合log4j2的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Java Map如何根據(jù)key取value以及不指定key取出所有的value
這篇文章主要介紹了Java Map如何根據(jù)key取value以及不指定key取出所有的value,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09java對(duì)同一個(gè)文件進(jìn)行讀寫(xiě)操作方法
在本篇文章里我們給大家詳細(xì)講述了java對(duì)同一個(gè)文件進(jìn)行讀寫(xiě)操作的方法和知識(shí)點(diǎn),需要的朋友們可以參考學(xué)習(xí)下。2018-10-10通過(guò)System.getProperty配置JVM系統(tǒng)屬性
這篇文章主要介紹了通過(guò)System.getProperty配置JVM系統(tǒng)屬性,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10