-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] 应用自定义 4xx 和 5xx 的方案 #1086
Comments
app/onerror.js 文件存放路径参考了 app/router.js 的思路。 |
再脑洞一个点,如果 onerror 想由一个 Controller 来统一处理呢?因为 render 很可能是在 Controller 定制实现的,而不是 ctx,这种怎么办? |
循环报错容灾一定要考虑在框架实现掉 |
这个其实挺严重的,如果在里面调用了 render,render 还是会报错。 |
思考了一下,应用的业务错误不应该放在 onerror 处理,两者还是有一些不同的。
所以我觉得业务的错误处理可以单独做一个插件,比如 egg-bizerror,这个插件提供如下功能 config/errorcode.js错误码配置,可以定义如下规范 module.exports = {
CODE_NAME: {
message: 'error message',
status: 'http status code',
},
}; ctx.throwBizError(code, addition)这个方法会 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 上。 ctx.responseBizError(err)这个方法会处理 throwBizError 抛出的异常,将其转化成响应,转换后已经不是一个异常了。如果 err 非 throwBizError 抛出则交给 onerror 处理。 响应规范,根据 {
"message": "",
"code": "",
"addition": ""
} 可以在任何地方使用此方法,如 try {
// 执行
} catch (err) {
ctx.responseBizError(err);
} middleware比较常用的是通过中间件,这样可以捕获所有的异常。所以插件可以提供一个中间件供配置
|
任何地方 throwBizError 是否不需要 responseBizError 也能被正确处理 biz error 并返回相应的 response? |
你说的就是中间件做的 |
就是要把内部 mbp / koi 用的那套业务错误码响应方式抽象出来。 |
现在提供的是中断抛出、拦截和响应,基本涵盖了每个步骤,有自定义就覆盖就好了。 关于错误码内容不是很强求,我看看能不能做到直接将 err 的属性放到 body 上。 |
弱弱的问下这个有进展没……等着用=。=||
|
@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 来返回对应的数据,这种情况错误处理也需要根据这种场景来返回相应的结果。 Spec错误定义种类错误分为三种未捕获异常、系统异常、业务异常,以下是分类比较
所有的类均继承自 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 。 |
egg 集成不好,比如插件要用就得依赖 egg,这样的依赖不是很合理。 |
是啊,所以很尴尬。 |
但在插件里面提供这些功能是合理的,要想个办法 |
这种只能是独立库,或者不要基类 |
最好提供一些简化的方法,例如 |
请问异常跟错误有什么区别呢 |
还有就是希望能够通过ctx拿到自己定义的所有的异常,这样就不用自己来管理了,不然各种require很乱。就像service一样就挺好 |
|
请教下,自定义的 Error 类文件放在项目的哪个位置合适呢? @popomore |
@104gogo 还没实现,你可以自己先放在 lib 目录吧。 |
Koa 其实有个
@fengmk2 第二点是按你楼顶的 RFC 做 ? |
我是从 #3593 在任何阶段终止流程并响应数据 过来的
实现 // 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'})
} 如果进入 这样即满足了我在 “任何阶段终止流程并响应数据” 的需求,又能利用 egg-onerror 漂亮�的错误页面。 |
@nimojs 基本就是这个思路。 |
请问这个需求大概什么时候会更新啊,有计划么? |
我继续跟进下 |
我的实现参考了之前写 springboot 的思路。 根据错误,自定义了 BadRequestError 400 等一系列 error。 service 遇到问题,就直接 throw 相应等 error。然后用中间件处理,转换成对应的状态码。 |
请问 |
可以看看这个 PR,就是加个中间件来处理,主要卡在文档上。还有现在应用不是很广,会有什么未知的问题还不知道。 |
所以现在的方案是把 |
是的,内置就是加一个中间件。 |
@popomore 请问那个 PR 除了文档还有啥问题嘛,什么时候可以直接用上呢。 我想把它 fork 出一份先用上,但是怎么替换内置的 |
回来备个注提供另外一种方案: 代码实现如下: 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传递信息的方式,有一定的心智负担,需要记住几点
另外:因为隐式指针,切记不要出现修改了指针变量。 |
Updated: 最新的提案看下面 @popomore 的回复: #1086 (comment)
--
目标
让应用自身可以定制这些特殊响应的功能,而不是通过 302 跳转到其他地方。
兼容性原则
如果应用没有开启此功能,则保持原来的是否方式不变化。
notfound 中间件 throw 404 error
将 notfound 的逻辑也统一到 egg-onerror 来处理。
应用通过
app/onerror.js
来配置自定义的处理逻辑框架和应用都可以覆盖
app/onerror.js
来实现统一处理逻辑。简写方式
不分状态码的统一 handler
支持 async function
支持标准 Controller 的方式
The text was updated successfully, but these errors were encountered: