Express異常捕獲

在使用Express框架設計web應用時,一定會遇到異常處理問題。
因爲用戶數據或代碼自身問題,常常引起請求時,控制器內部異常,res返回不能被執行,前臺表現爲請求“卡死”。

1 處理 Exception

1.1 Express 錯誤處理中間件

爲了避免這種情況,Express有“錯誤處理中間件”的機制。

const express = require('express');

process.on('unhandledRejection', rej => console.warn('全局捕獲Rejection', rej));

const app = express();

const router = express.Router();

router.get('/', function (req, res) {
    JSON.parse('{{'); // 拋出異常
    res.send('首頁');
});

app.use(router);

// 錯誤處理中間件
app.use(function (err, req, res, next) {
  console.warn('錯誤處理中間捕獲Exception', err);
  res.send('內部錯誤');
});

app.listen(5000, () => console.log('Server Start ...'));

上述代碼運行後,訪問http://localhost:5000時,會返回“內部錯誤”,表示控制器內錯誤被錯誤處理中間件捕獲。

2 處理 Rejection

2.1 存在的問題

AsyncFunction內部異常時,不會拋出Exception,而是Rejection,不能被傳統的try…catch…方式捕獲。
上述代碼如果改爲:

// function => AsyncFunction
router.get('/', async function (req, res) {
    JSON.parse('{{'); // 拋出異常
    res.send('首頁');
});

這時,訪問主頁,會發現卡死,並且後臺打印“全局捕獲Rejection…”。這表示異常不能被錯誤處理中間件捕獲,而是在全局被捕獲了。沒有人處理請求的返回了,於是請求卡死。

2.2 異常處理裝飾器

ctrlRejHandler.js :

module.exports = (ctrl) => {
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') throw Error('Try to use ctrlErrHandler to decorate an Object not a AsyncFunction');
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      console.warn('錯誤處理裝飾器 捕獲錯誤', rej);
      res.send('內部錯誤');
    });
  };
};

2.3 異常處理裝飾器的位置問題

理論上,我們應該爲每一個AsyncFunction類型的裝飾器都使用異常處理。也就是說,我們希望儘量在全局安裝裝飾器,而不是每次使用控制器時都添加。
一種更簡約的寫法是,在使用module.exports導出控制器時,包裝整個函數參數對象。但這會帶來一個問題:無法通過IDE的定位功能定位ctrl了。

容易想到,控制器就是用在路由中的,至少路由使用控制器時,默認進行包裝就好了。
可以定義一個函數來包裝router,實現這樣的功能。更改router.get/post/…等使用控制器的函數,對它們的最後一個回調函數參數(即控制器)進行錯誤處理。

routerRejHandler.js :

module.exports = routerRejHandler;

// 對ctrlRejHandler稍作修改,使其支持一般的function
function ctrlRejHandler(ctrl) {
  if (typeof ctrl !== 'function') throw Error('Try to use ctrlRejHandler to decorate an Object not a function');
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
    return ctrl;
  }
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      console.warn('錯誤處理裝飾器 捕獲錯誤', rej);
      res.send('內部錯誤');
    });
  };
};

// router裝飾器,自動對控制器進行錯誤捕獲
function routerRejHandler(router) {
  const methods = require('methods');
  [...methods, 'all'].forEach(method => {
    if (router[method]) {
      router[method] = function (path, ...fns) {
        const route = this.route(path);
        const ctrlIndex = fns.length - 1;
        // 只認爲最後一個回調函數參數爲控制器,之前的爲中間件
        fns[ctrlIndex] = ctrlRejHandler(fns[ctrlIndex]);
        route[method].apply(route, fns);
        return this;
      };
    }
  });
  return router;
}

使用之:

const express = require('express');
const router = routerRejHandler(express.Router());

這時,對router.get('/path', 中間件1, 中間件2, 控制器)這樣的情況,無需做任何其他處理,控制器拋出的Rejection將會被自動捕獲和處理。

進一步的,可以爲routerRejHandler增加一個回調函數,用以控制如何處理錯誤。

2.4 最佳實踐

rejHandler.js :

module.exports = {
  ctrlDecorator,
  routerDecorator
};

function ctrlDecorator(ctrl, dealer) {
  if ({}.toString.call(ctrl) !== '[object AsyncFunction]') {
    return ctrl;
  }
  return function (req, res, next) {
    return ctrl(req, res, next).catch(rej => {
      if (dealer) return dealer(req, res, next, rej);
      res.send('Internal Error');
    });
  };
}

function routerDecorator(router, dealer) {
  const methods = require('methods');
  [...methods, 'all'].forEach(method => {
    if (router[method]) {
      router[method] = function (path, ...fns) {
        if (fns.length === 0) return;
        const route = this.route(path);
        const ctrlIndex = fns.length - 1;
        if (typeof fns[ctrlIndex] !== 'function') throw Error('The last param should be a controller, but not a function');
        fns[ctrlIndex] = ctrlDecorator(fns[ctrlIndex], dealer);
        route[method].apply(route, fns);
        return this;
      };
    }
  });
  return router;
}

使用:

const routerRejHandler = require('./rejHandler').routerDecorator;

const router = routerRejHandler(express.Router(), function (req, res, next, rej) {
  console.warn('捕捉到控制器內Rejection', rej);
  res.status(500);
  res.send('內部錯誤');
});

另外,若不使用router,裝飾器同樣可以用於app對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章