史上最強egg框架的error處理機制
最強搬運工
異常處理
得益於框架支援的非同步程式設計模型,錯誤完全可以用 try catch
來捕獲。在編寫應用程式碼時,所有地方都可以直接用 try catch
來捕獲異常。
按照正常程式碼寫法,所有的異常都可以用這個方式進行捕獲並處理,但是一定要注意一些特殊的寫法可能帶來的問題。打一個不太正式的比方,我們的程式碼全部都在一個非同步呼叫鏈上,所有的非同步操作都通過 await 串接起來了,但是隻要有一個地方跳出了非同步呼叫鏈,異常就捕獲不到了。
如果 service.trade.check
方法中程式碼有問題,導致執行時丟擲了異常,儘管框架會在最外層通過 try catch
setImmediate
中的程式碼『跳出』了非同步鏈,它裡面的錯誤就無法被捕捉到了。因此在編寫類似程式碼的時候一定要注意。
框架也考慮到了這類場景,提供了 ctx.runInBackground(scope)
輔助方法,通過它又包裝了一個非同步鏈,所有在這個 scope 裡面的錯誤都會統一捕獲。
class HomeController extends Controller {
async buy () {
const request = {};
const config = await ctx.service.trade.buy(request) ;
// 下單後需要進行一次核對,且不阻塞當前請求
ctx.runInBackground(async () => {
// 這裡面的異常都會統統被 Backgroud 捕獲掉,並列印錯誤日誌
await ctx.service.trade.check(request);
});
}
}
了保證異常可追蹤,必須保證所有丟擲的異常都是 Error 型別,因為只有 Error 型別才會帶上堆疊資訊,定位到問題。
框架通過 onerror 外掛提供了統一的錯誤處理機制。對一個請求的所有處理方法(Middleware、Controller、Service)中丟擲的任何異常都會被它捕獲,並自動根據請求想要獲取的型別返回不同型別的錯誤(基於
onerror 外掛的配置中支援 errorPageUrl 屬性,當配置了 errorPageUrl 時,一旦使用者請求線上應用的 HTML 頁面異常,就會重定向到這個地址。
請求需求的格式 | 環境 | errorPageUrl 是否配置 | 返回內容 |
---|---|---|---|
HTML & TEXT | local & unittest | - | onerror 自帶的錯誤頁面,展示詳細的錯誤資訊 |
HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |
HTML & TEXT | 其他 | 否 | onerror 自帶的沒有錯誤資訊的簡單錯誤頁(不推薦) |
JSON & JSONP | local & unittest | - | JSON 物件或對應的 JSONP 格式響應,帶詳細的錯誤資訊 |
JSON & JSONP | 其他 | - | JSON 物件或對應的 JSONP 格式響應,不帶詳細的錯誤資訊 |
// config/config.default.js
module.exports = {
onerror: {
// 線上頁面發生異常時,重定向到這個頁面上
errorPageUrl: '/50x.html',
},
};
儘管框架提供了預設的統一異常處理機制,但是應用開發中經常需要對異常時的響應做自定義,特別是在做一些介面開發的時候。框架自帶的 onerror 外掛支援自定義配置錯誤處理方法,可以覆蓋預設的錯誤處理方法。
✋404
框架並不會將服務端返回的 404 狀態當做異常來處理,但是框架提供了當響應為 404 且沒有返回 body 時的預設響應。
-
當請求被框架判定為需要 JSON 格式的響應時,會返回一段 JSON:
{ "message": "Not Found" }
-
當請求被框架判定為需要 HTML 格式的響應時,會返回一段 HTML:
<h1>404 Not Found</h1>
但是能夠支援配置,將預設的 HTML 請求的 404 響應重定向到指定的頁面。
// config/config.default.js
module.exports = {
notfound: {
pageUrl: '/404.html',
},
};
自定義響應404
// app/middleware/notfound_handler.js 中介軟體
module.exports = () => {
return async function notFoundHandler(ctx, next) {
await next();
if (ctx.status === 404 && !ctx.body) {
if (ctx.acceptJSON) {
ctx.body = { error: 'Not Found' };
} else {
ctx.body = '<h1>Page Not Found</h1>';
}
}
};
};
配置中介軟體:
// config/config.default.js
module.exports = {
middleware: [ 'notfoundHandler' ],
};
統一錯誤處理——中介軟體的形式
Controller 和 Service 都有可能丟擲異常,這也是我們推薦的編碼方式,當發現客戶端引數傳遞錯誤或者呼叫後端服務異常時,通過丟擲異常的方式來進行中斷。
- Controller 中
this.ctx.validate()
進行引數校驗,失敗丟擲異常。 - Service 中呼叫
this.ctx.curl()
方法訪問 CNode 服務,可能由於網路問題等原因丟擲服務端異常。 - Service 中拿到 CNode 服務端返回的結果後,可能會收到請求呼叫失敗的返回結果,此時也會丟擲異常。
在 app/middleware
目錄下新建一個 error_handler.js
的檔案來新建一個 middleware
// app/middleware/error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的異常都在 app 上觸發一個 error 事件,框架會記錄一條錯誤日誌
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 生產環境時 500 錯誤的詳細錯誤內容不返回給客戶端,因為可能包含敏感資訊
const error = status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;
// 從 error 物件上讀出各個屬性,設定到響應中
ctx.body = { error };
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = status;
}
};
};
載入中介軟體config/config.default.js
// config/config.default.js
module.exports = {
// 載入 errorHandler 中介軟體
middleware: [ 'errorHandler' ],
// 只對 /api 字首的 url 路徑生效
errorHandler: {
match: '/api',
},
};
對error的型別進行判斷,返回自定義的message
- 框架級的錯誤,一般也不會丟給使用者的,egg-onerror 那邊兜底統一回復個資訊即可,主要還是看日誌來修復。
- 某些在你們業務中並不視為框架級的錯誤,是可以在 Service 層統一封裝丟擲的錯誤型別。
- 通用的錯誤可以在 egg-onerror 或者 自定義 Controller 基類裡面提供
throwBizErr
這類的方式去處理。
應用自定義
- onerror 主要處理全域性異常,這類基本都是未捕獲異常,也就是應用開發者不知道哪裡會拋異常,onerror 是用來兜底的。
- 業務錯誤一般是應用開發者已知的, 所以都會有對應的處理,常見的就是反回對應的錯誤文案。這些錯誤尤其不能出現在錯誤大盤上,應該使用其他的監控方式,比如 xxx 業務的成功率。
RFC:應用自定義 4xx 和 5xx 的方案
定製特殊響應的功能,而不是通過 302 跳轉到其他地方。
相容性的考慮
notfound throw 404 error
框架和應用都可以覆蓋 app/onerror.js
來實現統一處理邏輯。
- 優先選擇準確的 status handler
- 找不到就找 4xx,5xx 這種通用 handler
- 如果有 all,優先使用 all,否則根據 accepts 判斷來選擇 html,json
- 都找不到就找全域性預設 onerror 處理
// app/onerror.js
module.exports = {
'404': {
* html(ctx, err) {
// 這裡可以使用 render
yield ctx.render('404.html');
},
* json(ctx, err) {
// 不處理或者不配置或者返回 null, undefined,都會使用預設的 json 邏輯來處理
},
},
'403': function* (ctx, err) {
// all 的精簡版本寫法
},
'4xx': {
* all(ctx, err) {
// all 不區分 accepts,由開發者自行處理
},
},
};
錯誤分為三種未捕獲異常、系統異常、業務異常,以下是分類比較
定義 | 未捕獲異常 | 系統異常 | 業務錯誤 |
---|---|---|---|
類名 | Error | xxxException | xxxBizError |
說明 | js 內建錯誤,未做任何處理 | 自己丟擲的系統異常 | 自己丟擲的業務異常 |
錯誤處理方 | 由 onerror 外掛處理 | 業務可擴充套件處理 | 業務可擴充套件處理 |
可識別 | 否 | 是 | 是 |
屬性擴充套件 | 否 | 是 | 是 |
所有的類均繼承自Error類,並定義BaseError類,繼承自 BaseError 的錯誤是可以被識別的,而其他三方繼承 Error 的類都無法被識別。
類名只是用來區分三種錯誤,繼承可以自定義
業務錯誤處理封裝成外掛,比如egg-bizerror:
npm上的解釋:
usage:
// config/plugin.js
exports.bizerror = {
enable: true,
package: 'egg-bizerror',
};
// config/config.default.js
exports.bizerror = {
breakDefault: false, // disable default error handler禁用預設錯誤處理
sendClientAllParams: false, // return error bizParams to user,返回錯誤引數給使用者
interceptAllError: false, // handle all exception, not only bizError exception處理所有的異常,不僅是業務異常。
};
// config/errorcode.js
module.exports = {
'USER_NOT_EXIST': {
status: 400,
code: '400' // override code value,覆蓋code value。
message: 'can`t find user info',
errorPageUrl: '', // app will redirect this url when accepts is html
addtion1: 'a', // any, will return to browser 附件性的
},
'NOT_FOUND': {
errorPageUrl: (ctx, error) => {
return '/404.html';
}
}
'404': (ctx, error) => {
ctx.redirect('/404.html');
return false; // you can return false, break default logic,打斷預設的邏輯
}
}
API:
ctx.throwBizError(code, error, bizParams)–業務邏輯
throw an biz error
- code -
error.code
, defaultSYSTEM_EXCEPTION
, read errorcode config with this value when handle error. - error - error message or
Error
object. - bizParams -
error.bizParams
, extra data, can help you solve the problem.
bizParams還有下面的三個引數:
- bizParams.sendClient - object, this object will copy to the property
errors
of json object and send to client. - bizParams.code - it will cover
error.code
. - bizParams.log -
error.log
, if false, not log this error, defalut true.
// throw an error object
// error.code
// error.message
// error.log
// error.bizParams
// error.bizError
ctx.throwBizError('system_exception')
ctx.throwBizError(new Error())
ctx.throwBizError({ code: 'system_exception', log: false })
ctx.throwBizError('system_exception', { userId: 1, log: false })
ctx.throwBizError('system_exception', 'error message')
ctx.throwBizError('system_exception', new Error())
ctx.throwBizError(new Error(), { userId: 1, log: false })
ctx.throwBizError('system_exception', 'error message', { userId: 1, log: false })
try {
this.ctx.body = {
data: this.ctx.request.body,
};
throw new Error('hahahah');
} catch (error) {
this.ctx.throwBizError({ code: '-9999', userId: 1, log: false });
}
的結果是:
{"code":"-9999","message":"System Exception","errors":{"userId":1}}
-
ctx.responseBizError(error, bizParams)—返回響應
handle the error
- bizParams - supports the above
- bizParams.bizError - if you want the plugin to handle this error, you must be set
bizError: true
, otherwise, the plugin will throw this error.
第三種呼叫方法:
-
app.on(‘responseBizError’, (ctx, error) => {})
you can add listener to do some thing.
第四種重寫:
- app.BizErrorHandler - default handler class, you can override it
example:
// app/service/user.js
module.exports = app => {
class User extends app.Service {
async getUserId() {
let userInfo;
try {
userInfo = await this.getUser();
} catch (error) {
ctx.responseBizError(error, { bizError: true, code: 'USER_NOT_EXIST' })
return;
}
if (!userInfo || !userInfo.id) {
ctx.throwBizError('USER_NOT_EXIST');
}
return userInfo.id;
}
}
return User;
};
// app.js
// add handle logic
module.exports = app => {
app.on('responseBizError', (ctx, error) => {
if (error.bizParams && error.bizParams.bizType === 'getUser') {
errorCount++;
}
});
};
// app.js
// override default handler
module.exports = app => {
app.BizErrorHandler = class extends app.BizErrorHandler {
json(ctx, error, config) {
ctx.body = {
code: config.code,
msg: config.message,
};
}
}
};
egg-onerror:用來兜底
egg-onerror
預設在egg框架中。 但是你仍舊需要設定選項來匹配你的場景。.
errorPageUrl: String or Function
- 如果使用者請求html頁面在生產環境上,並丟擲了未捕獲的錯誤,他將定向到錯誤頁面errorPageUrl
.accepts: Function
- 檢測使用者的請求json
orhtml
.all: Function
- 定製錯誤處理器 如果all
存在, 其他的將被忽略.html: Function
- 定製html錯誤處理器.text: Function
- 定製text錯誤處理器.json: Function
- 定製json錯誤處理器.jsonp: Function
- 定製jsonp錯誤處理器.
/ config.default.js
// errorPageUrl support funtion
exports.onerror = {
errorPageUrl: (err, ctx) => ctx.errorPageUrl || '/500',
};
// an accept detect function that mark all request with `x-requested-with=XMLHttpRequest` header accepts json.
function accepts(ctx) {
if (ctx.get('x-requested-with') === 'XMLHttpRequest') return 'json';
return 'html';
}
一般性錯誤處理的原則:
- egg-onerror 是框架做兜底的
- 你自己的處理,可以在 Controller / Service 等地方自己 catch
- 或者通過 Middleware 結合 match 來做範圍的 catch
現在的錯誤處理外掛是 egg-onerror,但這個外掛主要是優雅處理未捕獲異常,也就是了為了讓應用不掛進行兜底,但是現在沒有一種統一的業務錯誤處理方案。
業務校驗:
比如引數校驗、業務驗證等等,這些並不屬於異常,一般會在響應時轉成對應的資料格式。常見的處理方式是介面返回錯誤,並在 response 轉換
class User extends Controller {
async show() {
const error = this.check(this.params.id);
if (error) {
this.ctx.status = 422;
this.ctx.body {
message: error.message,
};
return;
}
// 繼續處理
}
check(id) {
if (!id) return { message: 'id is required' };
}
}
異常型別的區分:
將已知異常和未捕獲異常做差異化處理。
例如狀態碼未捕獲時返回500,已知異常需要返回422等~
標準化響應:
如果業務丟擲自定義的系統異常和業務錯誤,可直接在錯誤處理裡面處理,未捕獲異常在 onerror 中處理。
繼承的錯誤可增加額外屬性,比如 HttpError 可增加 status 屬性作為處理函式的輸入。
欄位:
- 標準欄位包括
name: 一般為類名,如 NotFoundError
message: 錯誤的具體資訊,可讀的,如 404 Not Found
code: 大寫的字串,描述錯誤,如 NOT_FOUND
- http 擴充套件
status: http 狀態碼,400
- 錯誤處理的一般原則:
錯誤處理是最核心的功能,有如下規則
- 未捕獲異常不做處理,向上拋
- 系統異常會列印錯誤日誌,但是會按照標準格式 format
- 業務異常根據標準格式 format
- 根據內容協商,返回對應的 format 值
- 可自定義 format
egg-erros
errors for eggjs
提供兩種型別的錯誤:錯誤,異常
建立Error
const { EggError, EggException } = require('egg-errors');
let err = new EggError('egg error');
console.log(EggError.getType(err)); // ERROR
建立Exception
err = new EggException('egg exception');
console.log(EggException.getType(err)); // EXCEPTION
也能引入一個錯誤從普通的錯誤物件
err = new Error('normal error');
console.log(EggError.getType(err)); // BUILTIN
err = EggError.from(err);
console.log(EggError.getType(err)); // ERROR
錯誤也能被擴充套件:
const { EggBaseError } = require('egg-errors');
class CustomError extends EggBaseError {
constructor(message) {
super({ message, code: 'CUSTOM_CODE' });
}
}
或者使用ts能夠擴充套件錯誤選項:
import { EggBaseError, ErrorOptions } from 'egg-errors';
class CustomErrorOptions extends ErrorOptions {
public data: object;
}
class CustomError extends EggBaseError<CustomErrorOptions> {
public data: object;
protected options: CustomErrorOptions;
constructor(options?: CustomErrorOptions) {
super(options);
this.data = this.options.data;
}
}
建議使用message代替options在使用者的地方,他能夠很簡單的被開發者理解。
HTTP錯誤,是固有的錯誤,轉變成400~500狀態碼的錯誤物件,HTTPError擴充套件EggBaseError提供了兩個,status
和headers
.
const { ForbiddenError } = require('egg-errors');
const err = new ForbiddenError('your request is forbidden');
console.log(err.status); // 403
可獲得的錯誤:
BaseError
|- EggBaseError
| |- EggError
| |- HttpError
| | |- NotFoundError
| | `- ...
| `- CustomError
`- EggBaseException
|- EggException
`- CustomException
RFC:How To Create An Error
前置資料:
所有由egg和egg外掛以前的已知的error異常,都需要規範err.code。
意見建議稿:
- built-in Error
const errors = require('egg').errors;
const err = new errors.TypeError('ERR_EGG_SOME_ERROR_CODE_STRING', 'Some error haha');
console.log(err.code); // 'ERR_EGG_SOME_ERROR_CODE_STRING'
- custom Error
const errors = require('egg').errors;
// 使用'ERR_EGG_MY_ERROR'註冊一個新的子類
// 如果'ERR_EGG_MY_ERROR'存在, 將丟擲一個型別錯誤,錯誤碼是 'ERR_EGG_DUPLICATE_CODE'
errors.E('ERR_EGG_MY_ERROR', TypeError);
const err = new errors.ERR_EGG_MY_ERROR('my error here');
console.log(err.name); // 'TypeError'
console.log(err.code); // 'ERR_EGG_MY_ERROR'
message & code✋
錯誤包含兩個資訊,message&code,message是能夠改變的,但是他不將有大的改變,它有補丁或者微小的變化,程式碼首次出現後應保持穩定。
天豬大佬的話:
- 規範化錯誤名
- 報錯提示可以 i18n
- 便於開發者 google 檢索報錯
- 可以提供類似 angular 這樣的指引: [https://docs.angularjs.org/error/