1. 程式人生 > >史上最強egg框架的error處理機制

史上最強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)中丟擲的任何異常都會被它捕獲,並自動根據請求想要獲取的型別返回不同型別的錯誤(基於

Content Negotiation)。

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, default SYSTEM_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 or html.
  • 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

  • 錯誤處理的一般原則:

錯誤處理是最核心的功能,有如下規則

  1. 未捕獲異常不做處理,向上拋
  2. 系統異常會列印錯誤日誌,但是會按照標準格式 format
  3. 業務異常根據標準格式 format
  4. 根據內容協商,返回對應的 format 值
  5. 可自定義 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提供了兩個,statusheaders.

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/ c o m p i l e / b a d d i r ] ( h t t p s : / / d o c s . a n g u l a r j s . o r g / e r r o r / compile/baddir,即](https://docs.angularjs.org/error/