Fed dead && Rd won:浅谈Express和Koa的错误处理方式

我们从源码中反推,onerror实现方式。

Koa错误处理

koa分别从两个地方定义了onerror方法

application.js => app.onerror

源码如下:

  onerror(err) {
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 === err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
  }

见词明意

首先判断Error是否为一个原生的Error对象或者对应继承自Error的实例,如果不是,则会抛出一个TypeError,TypeError表示类型错误,在这里就是指传入的参数err类型错误。

官方原话:当 err.status 是 404 或 err.expose 是 true 时默认错误处理程序也不会输出错误。

silent为true也不会将stdout输出到控制台中。

err.stack,是Error对象中的一个属性,用来保存错误的堆栈信息。

很明显,出现什么错误的时候会调用此方法获取错误信息呢?

源码中有这样一段代码:

 if (!this.listenerCount('error')) this.on('error', this.onerror);

Application继承EmitterlistenerCount是NodeJS的Events模块中的方法,用来获取指定监听器的数量,如果没有获取到error事件(包含插件中error事件,例如:koa-onerror),则自动调用上面的onerror方法。

极少数情况下我们会重写app.onerror方法:

// 错误处理函数 
app.onerror = (err, ctx) => { 
console.error('server error', err);
};

满足相关条件,会自动调用,自动调用,自动调用

context.js => ctx.onerror

核心代码:

  onerror(err) {
    if (null == err) return;
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
    ...
 
    this.app.emit('error', err, this);
    ...
    const { res } = this;
    ....
 
    let statusCode = err.status || err.statusCode;

    // ENOENT support
    if ('ENOENT' === err.code) statusCode = 404;

    // default to 500
    if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

    // respond
    const code = statuses[statusCode];
    const msg = err.expose ? err.message : code;
    this.status = err.status = statusCode;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  },

在初始化app实例的时候,已经将app实例挂载到context.app

首先判断err类型,上面已经提到过。发射emit事件,通过on(下一小节介绍)监听。

respond,最终调用res.end函数 , res是我们熟知response对象,也就是原生的http.ServerResponse对象,他的实现过程非常的巧妙,在这里就不再仔细展开。

Node创建原生服务器会使用res.end() 直接关闭连接,msg错误信息一同会发送到客户端。

那么什么时候会使用此方法返回错误信息呢?

首先可以明确是在自定义的中间件中使用,举个例子:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.onerror(err)
  }
});
app.use(async (ctx, next) => {
  const order = await getOrderFromDB(ctx.params.id); // 假设这个函数会抛出异常
  ctx.body = order;
});

app.on('error', (err) => {
  console.error('server error', err);
});

上述第一个中间件,await等待响应,catch抛出异常,我们可以想一下,如果说当前中间件产生错误,会造成什么样的影响?

ctx.onerror(err):

Fed dead && Rd won:浅谈Express和Koa的错误处理方式

Fed dead && Rd won:浅谈Express和Koa的错误处理方式

上面的源码提到过,ctx.onerror发射了error事件,因此我们控制台可以打印出自定义的错误信息,同时服务因此出现错误

ctx.onerror(err):

控制台并无打印信息

响应404:

Fed dead && Rd won:浅谈Express和Koa的错误处理方式

未捕获到异常信息,服务会因此崩溃~

因此,他主要是捕获应用程序中未被 try/catch 包围的错误,例如在异步函数或回调函数中抛出的错误,以便能够捕获所有未被处理的错误并进行处理。

Emitter => app.on(‘error’)

上面提到过, 一是context.js发射error事件,二是application.js定义全局的onerror方法,通过on('error')监听. 它是继承Node中Emitter事件

如果对Node中的底层实现感兴趣,建议大家去读一读 死月老师的 《趣学NodeJS 》 小册

我们可以写个小案例:

const Koa = require('koa');
const jwt = require('jsonwebtoken');
const app = new Koa();
app.secret = 'my_secret_key';
app.on('error', (err, ctx) => {
  if (err.name === 'JsonWebTokenError') {
    ctx.status = 401;
    ctx.body = "无权限";
  } else {
    ctx.status = 500;
    ctx.body = { error: '错误' };
  }
});
app.use(async (ctx, next) => {
  try {
    //获取加密
    const token = ctx.headers.authorization.split(' ')[1];
    const decoded =await  jwt.verify(token, app.secret);
    ctx.state.user = decoded;
  } catch (err) {
    if (err.name == 'JsonWebTokenError') {
      console.log('JsonWebTokenError:', err.message);
      // throw err;
      ctx.app.emit('error', err, ctx);
    } else {
      throw err;
    }
  }
  
});
app.use(async (ctx) => {
  ctx.body = { message: 'Hello World!' };
});
app.listen(3000);

用Koa最常见的错误处理方式就是通过emit('error')on('error')处理, 你可以这样理解,自定义错误信息和集中式日志记录等:

const errHandle = (err, ctx) => {
  let status, message;
  switch (err.message) {
    case errTypes.NAME_OR_PASSWORD_IS_REQUIRED:
      status = 400; //bad request
      message = '用户名或者密码不能为空';
      break;
     ...
      break;
    default:
      status = 404;
      message = 'NOT FOUND';
  }
  ctx.status = status;
  ctx.body = message;
};
app.on('error', errHandle);

plugin=> koa-onerror

综合处理错误 中间件,使用方式非常简单, 详解配置可以去npm / githun 去查看相关资料:

const Koa = require('koa');
const onError = require('koa-onerror');
const app = new Koa();
onError(app);
app.use(async (ctx) => {
  throw new Error('error');
});

Express错误处理

express源码中对路由的处理部分代码:

   if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next); 
    }

router/index.js=> handle_error(err,…args)

Express源码解析这篇文章中, 我们未对错误处理的相关源码进行讲解,在这里顺便supply下。

在router/index.js里面对路径替换/匹配完毕后,最终会通过layerError(当前中间件中next传递过来的error)判断执行哪一种中间件调用方式。

此文肯定是执行layer.handle_error,可见当前方法中有四个参数,源码如下:

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

逻辑很简单,这里就不再解释。

fn(error, req, res, next);就是我们定义的全局错误中间件:

// error handler
app.use(function (err, req, res, next) {
  res.status(err.status || 500);
  res.render("error");
});

需要注意的是Express中的err,非Error对象也是可以的

lib/response.js=>sendfile/file.on(‘error’)

源码中还有对某个api特定的错误处理方法:res.sendFile

部分源码如下:

function sendfile(res, file, options, callback) {
  var done = false;
  var streaming;
  function onaborted() {
    ...
    callback(err);
  }
  // directory
  function ondirectory() {
  ...
    callback(err);
  }
  // errors
  function onerror(err) {
  ...
    callback(err);
  }
  // file
  function onfile() {
    streaming = false;
  }
  // finished
  function onfinish(err) {
    if (err && err.code === 'ECONNRESET') return onaborted();
    if (err) return onerror(err);
    if (done) return;
    setImmediate(function () {
      if (streaming !== false && !done) {
        onaborted();
        return;
      }
      if (done) return;
      done = true;
      callback();
    });
  }
  // streaming
  function onstream() {
    streaming = true;
  }
  file.on('error', onerror);
... 
}

其使用方式如下:

const express = require('express');
const app = express();
app.get('/download', function(req, res) {
  const filePath = '/path/to/file';
  const options = {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="file.pdf"'
    }
  };
  res.sendFile(filePath, options);
});

sendfile方法中的第二个参数file,其实是文件系统fs(Node/FileSystem)对象,他内部通过send的库实现的。

因此 file.on('error', onerror);即:


// 流的方式读取
const reader = fs.createReadStream("./foo.txt", {
  start: 0,
  end: 10,
  highWaterMark: 3
});
//  数据读取的过程
reader.on("data", (data) => {
  console.log(data);
  reader.pause();
  setTimeout(() => {
    reader.resume();
  }, 1000);
});

未捕获错误处理(扩展)

以Express为例,我们通过app.use((err,...args))捕获异常错误,但是我们来看个糟糕的例子:

app.get('/getUsers',(res,res) => {
process.nextTick(() => {
    throw new Error('delay')
    })
})

reader可以流七秒事件想一想,会发生什么?

答案:服务器彻底崩溃,以一种粗暴的形式关闭了整个服务器。

具体原因reader可以在评论区说出你们的idea。

那么我们如何优雅的关闭服务呢?

这里所谓的优雅不拯救,而是做好”投降”之前最后的挣扎。Node中提供了一个全局事件,用于捕获未被处理的异常,即没有被 try-catch 包裹或没有被其他方式处理的异常,上面的demo同理,也就是uncaughtException事件,实现方式如下:

process.on("uncaughtException",err => {
    console.log(err)
    //在这里做一些必要的清理工作, 例如关闭数据库、redis等。
    process.exit(1)
})

重要的提示一下, express中对于未捕获的异常所有仍然使用的uncaughtException事件,但是koa则不同,到这里呢~reader们应该有所恍然大悟,第一节代码中,koa中对于未捕获的抛出的错误竟然是404,我那里说崩溃,感觉差强人意 ~。同理在express中你不会再发现有关onerror事件了,统一都是app.use((err,...args))做处理,也就是说对于未捕获的是没有特定处理的(当然总会有神奇的第三方plugin去解决)。

总结

既是总结也是致谢。给reader们推荐一本关于Express的书籍 《Node与Express开发第二版》,他能让你有意想不到的收获!

Have good ideas, hurry up and practice! Great front -end engineers.

原文链接:https://juejin.cn/post/7214493929566961724 作者:thunderchen

(0)
上一篇 2023年3月26日 下午4:26
下一篇 2023年3月26日 下午4:36

相关推荐

发表回复

登录后才能评论