Updated: 最新的提案看下面 @popomore 的回复: https://github.com/eggjs/egg/issues/1086#issuecomment-413840182
--
让应用自身可以定制这些特殊响应的功能,而不是通过 302 跳转到其他地方。
如果应用没有开启此功能,则保持原来的是否方式不变化。
将 notfound 的逻辑也统一到 egg-onerror 来处理。
this.throw(404);
app/onerror.js 来配置自定义的处理逻辑框架和应用都可以覆盖 app/onerror.js 来实现统一处理逻辑。
// app/onerror.js
module.exports = app => {
return {
'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,由开发者自行处理
},
},
'500': {
* html(ctx, err) {
},
* json(ctx, err) {
},
},
'5xx': {
* html(ctx, err) {
},
* json(ctx, err) {
},
},
};
};
简写方式
// 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,由开发者自行处理
},
},
};
不分状态码的统一 handler
// 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,由开发者自行处理
},
},
};
// app/onerror.js
module.exports = function* all(ctx, err, status) {
// all 不区分 accepts 和 status,由开发者自行处理
};
这里只是扩展联想,API 没想好怎样设计
// app/onerror.js
module.exports = app => {
class ErrorController extends app.Controller {
* all(err, status) {
}
// 或者 async function 统一支持
async all(err, status) {
}
}
return ErrorController;
};
app/onerror.js 文件存放路径参考了 app/router.js 的思路。
再脑洞一个点,如果 onerror 想由一个 Controller 来统一处理呢?因为 render 很可能是在 Controller 定制实现的,而不是 ctx,这种怎么办?
循环报错容灾一定要考虑在框架实现掉
循环报错容灾一定要考虑在框架实现掉
这个其实挺严重的,如果在里面调用了 render,render 还是会报错。
思考了一下,应用的业务错误不应该放在 onerror 处理,两者还是有一些不同的。
所以我觉得业务的错误处理可以单独做一个插件,比如 egg-bizerror,这个插件提供如下功能
错误码配置,可以定义如下规范
module.exports = {
CODE_NAME: {
message: 'error message',
status: 'http status code',
},
};
这个方法会 throw 一个异常,可在应用任何地方抛出,可用于打断操作。
// 以前的打断逻辑
const errors = app.validator.validate();
if (errors) return;
// 新的打断逻辑
const errors = app.validator.validate();
if (errors) ctx.throwBizError('CODE_NAME', errors);
上面的写法其实不好看出问题,如果嵌套很深,前一种写法会在每一层都会判断是否异常,而后一种写法只需要通过 throw。
throwBizError 的时候会读取 errorcode 的配置,将里面的配置都放到 Error 对象上。addition 是额外的数据,开发者可以增加一些动态的数据,比如某某 ID,最后通过 responseBizError 添加到 response 上。
这个方法会处理 throwBizError 抛出的异常,将其转化成响应,转换后已经不是一个异常了。如果 err 非 throwBizError 抛出则交给 onerror 处理。
响应规范,根据 err.status 设置状态码
{
"message": "",
"code": "",
"addition": ""
}
可以在任何地方使用此方法,如
try {
// 执行
} catch (err) {
ctx.responseBizError(err);
}
比较常用的是通过中间件,这样可以捕获所有的异常。所以插件可以提供一个中间件供配置
// app/middleware/bizerror.js
module.exports = function() {
return function* (next) {
try {
yield next;
} catch (err) {
this.responseBizError(err);
}
};
};
任何地方 throwBizError 是否不需要 responseBizError 也能被正确处理 biz error 并返回相应的 response?
你说的就是中间件做的
就是要把内部 mbp / koi 用的那套业务错误码响应方式抽象出来。
现在提供的是中断抛出、拦截和响应,基本涵盖了每个步骤,有自定义就覆盖就好了。
关于错误码内容不是很强求,我看看能不能做到直接将 err 的属性放到 body 上。
弱弱的问下这个有进展没……等着用=。=||
有个疑问,jsonp如何处理?有两个方案:
@beliefgp 可以提个 jsonp pr 看看
@fengmk2 千总……提了,有空看看
老哥们……看你们迟迟没动静……我先写了个egg-bizerror,有空帮看看
看来还是很难统一,看看社区是否能够接受 egg-bizerror
现在的错误处理插件是 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' };
}
}
但是业务场景是非常复杂的,可能在 controller 里面调用多层 service,这样就必须把错误结果一层层传递。所以这种场景业务校验推荐使用异常的方式,类似上面的场景只需要抛出一个异常
class User extends Controller {
async show() {
this.check(this.params.id);
// 继续处理
}
check(id) {
if (!id) throw new Error('id is required');
}
}
然后再中间件处理这个异常
上面的示例也同样抛出 Error,如果不写中间件处理同样会走到 onerror 插件,根据规则会打印错误日志并返回 500 。
这不是我们期望的,开发者希望返回正确的格式,比如 status 是 422,body 是一个含错误信息的 json。所以我们需要明确已知异常和未捕获异常,并对他们做差异处理。
如果在写一个 api server 的时候,希望响应格式是规范的,而开发者一般都比较关注正常结果,异常时会返回各种格式,所以对于一个 api server 来说这也是非常重要的。
有些应用会根据 content-type 来返回对应的数据,这种情况错误处理也需要根据这种场景来返回相应的结果。
错误分为三种未捕获异常、系统异常、业务异常,以下是分类比较
定义 | 未捕获异常 | 系统异常 | 业务错误
--- | --- | --- | ---
类名 | Error | xxxException | xxxBizError
说明 | js 内置错误,未做任何处理 | 自己抛出的系统异常 | 自己抛出的业务异常
错误处理方 | 由 onerror 插件处理 | 业务可扩展处理 | 业务可扩展处理
可识别 | 否 | 是 | 是
属性扩展 | 否 | 是 | 是
类名只是用来区分三种错误,继承可以自定义
所有的类均继承自 Error 类,并定义 BaseError 类,继承自 BaseError 的错误是可以被识别的,而其他三方继承 Error 的类都无法被识别。
class BaseError extends Error {}
class HttpClientError extends BaseError {}
class HttpServerError extends BaseError {}
BaseError.check(BaseError); // true
BaseError.check(Error); // false
如果业务抛出自定义的系统异常和业务错误,可直接在错误处理里面处理,未捕获异常在 onerror 中处理。
继承的错误可增加额外属性,比如 HttpError 可增加 status 属性作为处理函数的输入。
标准字段包括
http 扩展
自行在代码里面引入对应的类
import { http } from 'egg-errors';
class User extends Controller {
async show() {
this.check(this.params.id);
// 继续处理
}
check(id) {
if (!id) throw new http.UnprocessableEntityError('id is required');
}
}
自定义类
import { BaseError } from 'egg-errors';
class CustomError extends BaseError {
constructor(message) {
super(message);
this.code = 'CUSTOM_ERROR';
}
}
throw new CustomError('xxx');
错误处理是最核心的功能,有如下规则
标准 format
{
"code": "",
"message": ""
}
业务异常是否叫 xxxBizError 根据合适?要不然很难区分 js 内置的 TypeError 等 xxxError 命名的异常。
这里不是根据 name 来区分的,是根据继承链路,这个类名主要用来区分三种错误。
有业务处理,意思是业务来 throw error 吧?最终处理层还是在 onerror 吧?
@fengmk2 提供业务做 format,如果标准输出不符合要求,可以根据 err 对象的数据自行输出?我修改了描述“业务可扩展处理”
有个地方不是很清楚,系统和业务之间的边界是什么?是「Chair vs. 业务逻辑」还是「Chair 以及生态比如插件 vs. 业务逻辑」?
我理解系统错误(Exception)是相对未捕获异常而言的,比如一个底层模块抛出的错误是 Error,这时错误处理函数无法识别这个错误,所以可以在调用这个模块的时候捕获并创建一个系统错误,这样就可以识别了。
这里需要明确的是未捕获异常不会在业务的错误处理里面处理,会直接到 onerror 处理,所以一般业务的异常需要包一层来做到统一处理。
一般用法
class InternalException extends BaseException {}
try {
// call method
} catch (err) {
throw InternalException.from(err);
}
@popomore 就按你的这个 rfc,以应用代码的角度,先写一个 example 看看?
egg-errors 独立一个库这个没问题,不过我期望是在 egg 里面是集成进去,而不是开发者需要手动安装和import 。
cc @dead-horse https://github.com/eggjs/egg/issues/1086#issuecomment-413840182
egg 集成不好,比如插件要用就得依赖 egg,这样的依赖不是很合理。
不能 exports 出来? 就像 egg.Controller 那样,提供 egg.Error 就可以给插件用了吧?
不集成也不好的,如果到时候重构一个大版本,或者用户不小心锁版本之类的造成引入多个 egg-errors 可能造成一些特殊的问题。
我倾向于如要要做就要做成内置的,然后从 app 上获取这种模式
不能 exports 出来? 就像 egg.Controller 那样,提供 egg.Error 就可以给插件用了吧?
那相当于插件要在 dependencies 配 egg 了。
这个库没有特殊逻辑,基本上应用都不会直接使用这个 error 的,都需要继承来定义自己的 error,然后处理也是要自己定义 format,否则还是走到 onerror。
总的来说这是一个方案,不是一个功能。
那相当于插件要在 dependencies 配 egg 了。
嗯,这个不应该配置,要看有没有其他方式,但现在不是很推荐用 app.XX 的方式
我们现在是支持在插件里面提供 Service 的,好像只能用 app => class XService extends app.Service {} 的方式?
我们现在是支持在插件里面提供 Service 的,好像只能用 app => class XService extends app.Service {} 的方式?
是啊,所以很尴尬。
但在插件里面提供这些功能是合理的,要想个办法
这种只能是独立库,或者不要基类
new http.UnprocessableEntityError('message')
最好提供一些简化的方法,例如 new http.E422('params error')
请问异常跟错误有什么区别呢
还有就是希望能够通过ctx拿到自己定义的所有的异常,这样就不用自己来管理了,不然各种require很乱。就像service一样就挺好
请教下,自定义的 Error 类文件放在项目的哪个位置合适呢? @popomore
@104gogo 还没实现,你可以自己先放在 lib 目录吧。
Koa 其实有个 ctx.throw([status], [msg], [properties])
- [x] egg-errors 统一的 Error 方案 #1086 (comment)
- [ ] egg-onerror 改造支持 egg-errors @popomore
@fengmk2 第二点是按你楼顶的 RFC 做 ?
我是从 https://github.com/eggjs/egg/issues/3593 在任何阶段终止流程并响应数据 过来的
转了一圈,最终我用了 middleware 的方式解决了自己抛出业务错误json的需求
我的需求其实是实现 PHP 的
die()
实现 die
// module/res/index
function die (data) {
let error = new DieError(JSON.stringify(data))
throw error
}
class DieError extends Error {}
实现中间件
// middleware/error.js
import { DieError } from "../../module/res/index"
module.exports = () => {
return async function (ctx, next) {
try {
await next();
}
catch(err) {
if (err.constructor === DieError) {
ctx.status = 200
ctx.set('Content-Type', 'text/json')
return ctx.body = JSON.parse(err.message)
}
throw err
}
}
};
model 层中断并响应(可以是其他任何层)
let user = await user.find(query)
if (!user) {
die({type: 'fail', code: 'USER_NOT_EXIST'})
}
如果进入 !user 逻辑。浏览器会响应 {"type":"fail","code":"USER_NOT_EXIST"} 并中断后续操作
这样即满足了我在 “任何阶段终止流程并响应数据” 的需求,又能利用 egg-onerror 漂亮的错误页面。
@nimojs 基本就是这个思路。
请问这个需求大概什么时候会更新啊,有计划么?
我继续跟进下
我的实现参考了之前写 springboot 的思路。
根据错误,自定义了
BadRequestError 400
UnauthorizedError 401
ForbiddenError 403
NotFoundError 404
等一系列 error。
service 遇到问题,就直接 throw 相应等 error。然后用中间件处理,转换成对应的状态码。
请问 eggjs/egg-errors 这个是官方后续的解决方案么,如果是的话,我就直接用了,以后再跟着升级就好了。
https://github.com/eggjs/egg-onerror/pull/29
可以看看这个 PR,就是加个中间件来处理,主要卡在文档上。还有现在应用不是很广,会有什么未知的问题还不知道。
所以现在的方案是把 egg-errors 配合 egg-onerror 内置在 egg 了吗?
是的,内置就是加一个中间件。
@popomore 请问那个 PR 除了文档还有啥问题嘛,什么时候可以直接用上呢。
我想把它 fork 出一份先用上,但是怎么替换内置的 egg-onerror?
回来备个注提供另外一种方案:
代码实现如下:
class Out {
constructor() {
this.fail = false
this.message = ""
}
end(...messageList) {
this.fail = true
this.message = messageList.join("")
}
failReply() {
return {
type: "fail",
msg: this.message,
}
}
}
function homeCtrl(req){
let out = new Out()
aService(req.id, out)
// 控制层要检查out,并响应 out.failReply
if (out.fail) {
return out.failReply()
}
return {type:"pass"}
}
function aService(id, out){
if (id === "1") {
out.end("can not be 1") ; return
}
bService(id, out) ; if (out.fail) return // 如果出现代码 函数名(参数1, out) ,在 out) 后面必须要出现 if (out.fail) return
}
function bService(id, out){
if (id === "2") {
// 注意 js是隐式指针,所以如果 重新赋值 out,将会导致“out指针失效”, 如果增加 out = new Out() 会导致错误无法传递
out.end("can not be 2") ; return
}
}
console.log(
homeCtrl({id: "1"})
) // {type: "fail", msg: "can not be 1"}
console.log(
homeCtrl({id: "2"})
) // {type: "fail", msg: "can not be 2"}
console.log(
homeCtrl({id: "3"})
) // {type: "pass"}
function cService(idList, out) {
// 有些场景下如果在回调函数使用了 out.end 需要注意检查
idList.some(function(id) {
if (id == "c") {
out.end("不能是c") ; return true
}
}) ; if (out.fail) return
}
一些用户正常操作,但是被拒绝的请求是应该给到友好的消息的,这些消息使用 out传递,如果是无法预料的异常,且错误信息不能暴露在响应中,则采取500,并隐藏错误信息。
这样可以让 try catch 捕获的都是异常,而不是业务错误。
当然这种out传递信息的方式,有一定的心智负担,需要记住几点
另外:因为隐式指针,切记不要出现修改了指针变量。
Most helpful comment
我是从 https://github.com/eggjs/egg/issues/3593 在任何阶段终止流程并响应数据 过来的
转了一圈,最终我用了 middleware 的方式解决了自己抛出业务错误json的需求
实现
die实现中间件
model 层中断并响应(可以是其他任何层)
如果进入
!user逻辑。浏览器会响应{"type":"fail","code":"USER_NOT_EXIST"}并中断后续操作这样即满足了我在 “任何阶段终止流程并响应数据” 的需求,又能利用 egg-onerror 漂亮的错误页面。