史上最强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 Foundh1>
    

但是能够支持配置,将默认的 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 = '

Page Not Found

'
; } } }; };

配置中间件:

// 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/ compile/baddir](https://docs.angularjs.org/error/compile/baddir%EF%BC%8C%E5%8D%B3) error 输出简单的提示,并输出一条链接,开发者点击后可以访问官网看详细的指引。

koa中的ctx( ctx.throw() )

koa context包装了request和response对象在,并提供了非常有用的方法来写api和web应用。这些操作疆场用在HTTP服务开发,在这个级别上添加而不是更高级别的框架,迫使中间件执行这个普通的函数。

一个context创建每个request,在中间件中医用座位接收者或者ctx识别,示例:

app.use(async ctx => {
  ctx; // is the Context
  ctx.request; // is a Koa Request
  ctx.response; // is a Koa Response
});

很多context的存取和方法都可表示为ctx.requestorctx.response,为了使用起来方便,是相似的。ctx.type&ctx.length代表着responsectx.path&ctx.method代表着request

ctx.req

ctx.res

避免使用node中的属性:

  • res.statusCode
  • res.writeHead()
  • res.write()
  • res.end()

ctx.request&ctx.response是koa中的object

ctx.state

推荐的命名空间,通过中间件和前端视图来进行传递信息。

ctx.state.user = await User.find(id);

ctx.app

ctx.cookies.get(name[,options])

signed cookie应该被签名

koa使用cookie模块选项简单通过。

ctx.cookies.set(name,value,[options])

设置cookie得键值对的选项:

  • maxAge 以毫秒为单位设置过期时间
  • signed 签名cookie的值
  • expires 一个日期为cookie的满期
  • path cookie path, /' by default
  • domain cookie域
  • secure secure cookie
  • httpOnly server-accessible cookie, true by default
  • overwrite 一个boolean值,是否覆盖以前设置的同名cookie,默认为false,如果设置为true,所有的cookie都会被同名的替代,无论是path还是域

ctx.throw([status,msg,propertioes])

抛出错误,默认是500,

ctx.throw(400);
ctx.throw(400, 'name required');
ctx.throw(400, 'name required', { user: user });

相同的效力:

const err = new Error('name required');
err.status = 400;
err.expose = true;
throw err;

这是用户级的错误,并使用err.expose标记,消息适合客户端的响应,这通常不是错误信息,因为不想泄露信息。

ctx.throw(401, 'access_denied', { user: user });

您可以选择传递一个属性对象,该对象按原样合并到错误中,对于装饰向上游请求者报告的机器友好错误很有用。

ctx.assert(value,[status],[msg],[properties])

ctx.assert(ctx.state.user, 401, 'User not found. Please login!');

ctx.response

能够设为ctx.response=false

如果要写入原始res对象而不是让Koa为您处理响应,请使用此选项。

request aliases

  • ctx.header
  • ctx.headers
  • ctx.method
  • ctx.method=
  • ctx.url
  • ctx.url=
  • ctx.originalUrl
  • ctx.origin
  • ctx.href
  • ctx.path
  • ctx.path=
  • ctx.query
  • ctx.query=
  • ctx.querystring
  • ctx.querystring=
  • ctx.host
  • ctx.hostname
  • ctx.fresh
  • ctx.stale
  • ctx.socket
  • ctx.protocol
  • ctx.secure
  • ctx.ip
  • ctx.ips
  • ctx.subdomains
  • ctx.is()
  • ctx.accepts()
  • ctx.acceptsEncodings()
  • ctx.acceptsCharsets()
  • ctx.acceptsLanguages()
  • ctx.get()

response aliases

  • ctx.body
  • ctx.body=
  • ctx.status
  • ctx.status=
  • ctx.message
  • ctx.message=
  • ctx.length=
  • ctx.length
  • ctx.type=
  • ctx.type
  • ctx.headerSent
  • ctx.redirect()
  • ctx.attachment()
  • ctx.set()
  • ctx.append()
  • ctx.remove()
  • ctx.lastModified=
  • ctx.etag=

2018年11月22日

javascript中的Error知识

  • 通过Error的构造器可以创建一个错误对象。当运行时错误产生时,Error的实例对象会被抛出。

Error对象也可用于用户自定义的异常的基础对象

  • new Error([message[,filename[,lineNumber]]])

filename:默认是调用Error构造器代码所在的文件 的名字

lineNumber:默认是调用Error构造器代码所在的文件的行号

  • 描述:

当代码运行时的发生错误,会创建新的Error对象,并将其抛出。

  • 除了通用的Error构造函数外,JavaScript还有6个其他类型的错误构造函数。

EvalError:创建一个error实例,表示错误的原因:与 eval() 有关。

InternalError:创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: “递归太多”.

RangeError:创建一个error实例,表示错误的原因:数值变量或参数超出其有效范围.

ReferenceError:创建一个error实例,表示错误的原因:无效引用。

SyntaxError:创建一个error实例,表示错误的原因:eval()在解析代码的过程中发生的语法错误。

TypeError:创建一个error实例,表示错误的原因:变量或参数不属于有效类型。

URIError:创建一个error实例,表示错误的原因:给 encodeURI()decodeURl()传递的参数无效。

  • 标准属性:

Error.prototype.constructor

Error.prototype.message

Error.prototype.name

厂家扩展:

Microsoft

Error.prototype.description

类似于message

Error.prototype.number

错误码

Mozilla

Error.prototype.fileName

产生该错误的文件名

Error.prototype.lineNumber

产生该错误的行号

Error.prototype.columnNumber

产生该错误的列号。

Error.prototype.stack

错误堆栈

  • 方法

Error.prototype.toSource()

返回一个包含特定 Error 对象的源代码字符串,你可以用该值新建一个新的对象,重写自 Object.prototype.toSource() 方法。

Error.prototype.toString()

返回一个表示该对象的字符串,重写自 Object.prototype.toString() 方法

处理基本错误

try {
    throw new Error("Whoops!");
} catch (e) {
    alert(e.name + ": " + e.message);
}

处理特定的错误:

判断异常的类型来特定处理某一类的异常,即判断 constructor 属性,当使用现代Javascript引擎时,可使用instanceof 关键字:

try {
    foo.bar();
} catch (e) {
    if (e instanceof EvalError) {
        alert(e.name + ": " + e.message);
    } else if (e instanceof RangeError) {
        alert(e.name + ": " + e.message);
    }
    // ... etc
}

共有7个类型的错误构造函数,上述。

自定义异常类型:

自定义基于Error的异常类型,使得你能够 throw new MyError() 并可以使用 instanceof MyError 来检查某个异常的类型. 这种需求的通用解决方法如下.

注意,在FireFox中抛出自定义类型的异常会显示不正确的行号和文件名。

// Create a new object, that prototypally inherits from the Error constructor.
function MyError(message) {
  this.name = 'MyError';
  this.message = message || 'Default Message';   this.stack = (new Error()).stack;
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

try {
  throw new MyError();
} catch (e) {
  console.log(e.name);     // 'MyError'
  console.log(e.message);  // 'Default Message'
}

try {
  throw new MyError('custom message');
} catch (e) {
  console.log(e.name);     // 'MyError'
  console.log(e.message);  // 'custom message'
}

你可能感兴趣的:(javascript,error,node,js,egg)