服务端的错误/异常类型:
操作错误:非程序 bug 导致的运行时错误。如:数据库连接失败、请求接口超时、系统内存用光等等。
程序错误:程序 bug 导致的错误,只要修改代码就可以避免。如:尝试读取未定义对象的属性、语法错误等等。
很显然,我们真正需要处理的是操作错误,而程序错误在开发阶段就应该马上修复。
那怎么处理操作错误呢?总结一下大概有如下这些方法:
直接处理。举个例子:尝试向一个文件中写入东西,但是这个文件不存在,那处理这个错误的方法就是先创建好要写入的文件。如果我们知道怎么处理错误,那直接处理就是。
重试。有时某些错误可能是偶发的(比如服务器连接不稳定等),我们可以尝试对当前操作进行重试。但是一定要设置重试的超时时间、次数,避免长时间的等待卡死应用。
直接将错误抛给调用方。如果不知道具体怎么处理错误,最简单的就是将错误往上抛。比如:检查到用户没有访问某个资源的权限,那我们直接
throw Error(403, '没有权限')
(带上 status 状态码比较好),上层代码就可以 catch 这个错误。要么展示一个统一的无权限页面给用户,或者返回一个错误 json 给调用方。写日志然后将错误抛出。这种情况一般是发生了比较致命的错误,没法处理,也不能重试,那就需要记下错误日志(方便以后定位问题),然后将错误往上抛(交给上层代码去进行统一的错误展示)。
使用中间件统一处理错误
我们可以使用强大的中间件,来实现在 koa 里优雅地对错误进行统一处理。
我们先定义一个catchError
中间件:
// src/middlewares/catchError.js
const catchError = async (ctx, next) => {
try {
await next()
if (ctx.status === 404) { // 只是一个抛出自定义特定错误的示例
throw new errs.NotFound()
}
} catch(err) {
if (err.errorCode) {
// 如果是自己主动抛出的 HttpException类 错误
ctx.status = err.status || 500
ctx.body = {
code: err.code,
message: err.message,
errorCode: err.errorCode,
request: `${ctx.method} ${ctx.path}`,
}
} else {
// 触发 koa app.on('error') 错误监听事件,可以打印出详细的错误堆栈 log
ctx.app.emit('error', err, ctx)
}
}
});
对 Error 分类
HttpException
类是继承自Error
的一个构造器。我们可以将错误进行细分,定义一些特定类型的异常类,从而更精细地对异常进行处理。代码如下:
// src/constant/http-exception.js
class HttpException extends Error {
// message为异常信息,errorCode为错误码(开发人员内部约定),code为HTTP状态码
constructor(message = '服务器异常', errorCode = 10000, code = 400) {
super()
this.errorCode = errorCode || 10000
this.code = code || 400
this.message = message || '服务器异常'
}
}
class ParameterException extends HttpException {
constructor(message, errorCode) {
super()
this.errorCode = errorCode || 10000
this.code = 400
this.message = message || '参数错误'
}
}
class NotFound extends HttpException {
constructor(message, errorCode) {
super()
this.errorCode = errorCode || 10001
this.code = 404
this.message = message || '资源未找到'
}
}
class AuthFailed extends HttpException {
constructor(message, errorCode) {
super()
this.errorCode = errorCode || 10002
this.message = message || '授权失败'
this.code = 401
}
}
class Forbidden extends HttpException {
constructor(message, errorCode) {
super()
this.errorCode = errorCode || 10003
this.message = message || '禁止访问'
this.code = 403
}
}
module.exports = {
HttpException,
ParameterException,
NotFound,
AuthFailed,
Forbidden,
}
error
错误侦听器
上面catchError
中间件中,我们让HttpException
类之外的错误都手动触发koa
的error
侦听器,写一下事件处理函数:
// src/utils/errorHandler.js
const path = require('path');
const fs = require('fs');
const escapeHtml = require('escape-html');
const isDev = env === 'development';
const templatePath = isDev
? path.join(__dirname, 'templates/dev_error.html')
: path.join(__dirname, 'templates/prod_error.html');
const defaultTemplate = fs.readFileSync(templatePath, 'utf8');
export default function errorHandler(err, ctx) {
console.log('onerror', err)
// 未知异常状态,默认使用 500
ctx.status = err.status || 500
// 获取客户端请求接受类型
// ctx.accepts 是 request.accepts 的别名,即客户端可接受的内容类型。
// 和其他协商 API 一样, 如果没有提供类型(没有传参数),则返回 所有 客户端可接受的类型。[ '*/*' ]
// 如果提供了,就返回最佳匹配,即第一个匹配上的。
// console.log(ctx.accepts())
switch (ctx.accepts('json', 'html', 'text')) {
case 'json':
// ctx.type 是 response.type 的别名, 用于设置响应头 Content-Type
ctx.type = 'application/json'
ctx.body = { code: ctx.status, message: err.message }
break
case 'html':
ctx.type = 'text/html'
ctx.body = defaultTemplate
.replace('{{status}}', escapeHtml(err.status))
.replace('{{stack}}', escapeHtml(err.stack));
break
case 'text':
ctx.type = 'text/plain'
ctx.body = err.message
break
default:
ctx.throw(406, 'json, html, or text only')
}
}
然后在app.js
中将catchError
中间件置于最上方(所有其他中间件之前,鉴于 koa 的洋葱圈原型);
把包含HttpException
类的对象作为常量挂在全局,并用app.on('error', errorHandler)
监听非自定义错误。
const path = require('path')
const Koa = require('koa')
const serve = require('koa-static')
const logger = require('koa-logger')
const koaBody = require('koa-body')
const config = require('config')
const mongoose = require('mongoose')
const catchError = require('@/middlewares/catchError')
import errorHandler from '@/utils/errHandler.js'
import adminRouter from '@/routes/admin'
import indexRouter from '@/routes/index'
// 全局定义一些异常类型,方便针对性抛出
const errors = require('@/constant/http-exception')
global.errs = errors
const app = new Koa()
/* koa 是洋葱模型
一个请求匹配到的所有中间件,从【第一个】开始执行,一旦执行到 await next() 的时候就会暂停,进入到下一个匹配的中间件;
到【最后一个】再回过头来倒着处理每个中间件 await 后面的程序,直到执行到【第一个】中间件。
*/
// 错误捕获中间件 await next() 后的内容会在最后执行,所以我们要把它放在最前面,这一点和 express 非常不同
app.use(catchError)
mongoose
.connect(
`mongodb://${config.get('db.user')}:${config.get('db.pwd')}@${config.get(
'db.host'
)}:${config.get('db.port')}/${config.get('db.name')}`
)
.catch(err => {
console.log(err)
throw new errs.HttpException('数据库连接失败')
})
// 静态资源服务中间件
app.use(serve(path.join(process.cwd(), 'public')))
// 记录日志中间件
app.use(logger())
// 处理 post 请求参数的中间件
app.use(koaBody())
// 注册管理后台路由中间件
app.use(adminRouter.routes()).use(adminRouter.allowedMethods())
// 注册前台路由中间件
app.use(indexRouter.routes())
// 错误监听器
app.on('error', errorHandler)
app.listen(3003, err => {
if (err) throw err
console.log('runing at 3003')
})
比如我们在路由处理程序中要抛出这些特定类型异常(我们定义好的HttpException
类):
const Router = require('koa-router')
const router = new Router()
router.post('/user', (ctx, next) => {
if(true){
throw new errs.HttpException('网络请求错误', 10001, 400)
}
})
catchError
中间件中写的'throw new errs.NotFound()'
同理。
完整的代码 GitHub 地址是 https://github.com/aizawasayo/animal_server.git,仍在更新维护中,仅供参考。