feat: error handle middleware (#214)

* feat: error handle middleware

* feat: application error handler

* feat: handle with sequelizeValidationError

* fix: test

* fix: test
This commit is contained in:
ChengLei Shao 2022-03-02 12:50:15 +08:00 committed by GitHub
parent 1c6289dd88
commit 86065fa208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 9 deletions

View File

@ -7,14 +7,7 @@ jest.setTimeout(300000);
// 把 console.error 转换成 error方便断言 // 把 console.error 转换成 error方便断言
(() => { (() => {
const spy = jest.spyOn(console, 'error'); const spy = jest.spyOn(console, 'error');
beforeAll(() => {
spy.mockImplementation((message) => {
console.log(message);
throw new Error(message);
});
});
afterAll(() => { afterAll(() => {
spy.mockRestore(); spy.mockRestore();
}); });
})(); })();

View File

@ -0,0 +1,41 @@
import { mockServer } from '@nocobase/test';
describe('create with exception', () => {
it('should handle validationError', async () => {
const app = mockServer();
app.collection({
name: 'users',
fields: [
{
name: 'name',
type: 'string',
allowNull: false,
},
],
});
await app.loadAndInstall();
const response = await app
.agent()
.resource('users')
.create({
values: {
title: 't1',
},
});
expect(response.statusCode).toEqual(400);
expect(response.body).toEqual({
errors: [
{
message: 'users.name cannot be null',
},
],
});
await app.destroy();
});
});

View File

@ -5,7 +5,6 @@ import * as actions from './actions/users';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
export default class UsersPlugin extends Plugin { export default class UsersPlugin extends Plugin {
async beforeLoad() { async beforeLoad() {
this.db.on('users.afterCreateWithAssociations', async (model, options) => { this.db.on('users.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options; const { transaction } = options;
@ -85,5 +84,4 @@ export default class UsersPlugin extends Plugin {
}, },
}); });
} }
} }

View File

@ -0,0 +1,48 @@
import { mockServer } from '@nocobase/test';
describe('error handle', () => {
it('should handle error with default handler', async () => {
const app = mockServer();
app.use(async () => {
throw new Error('some thing went wrong');
});
const response = await app.agent().post('/');
expect(response.statusCode).toEqual(500);
expect(response.body.errors[0].message).toEqual('some thing went wrong');
});
it('should handle error by custom handler', async () => {
class CustomError extends Error {
constructor(message, errors) {
super(message);
this.name = 'CustomError';
}
}
const app = mockServer();
app.errorHandler.register(
(err) => {
return err.name == 'CustomError';
},
(err, ctx) => {
ctx.body = {
message: 'hello',
};
ctx.status = 422;
},
);
app.use(async () => {
throw new CustomError('some thing went wrong', []);
});
const response = await app.agent().post('/');
expect(response.statusCode).toEqual(422);
expect(response.body).toEqual({ message: 'hello' });
});
});

View File

@ -12,6 +12,7 @@ import { createACL } from './acl';
import { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper'; import { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper';
import { Plugin } from './plugin'; import { Plugin } from './plugin';
import { PluginManager, InstallOptions } from './plugin-manager'; import { PluginManager, InstallOptions } from './plugin-manager';
import { ErrorHandler } from './error-handler';
export interface ResourcerOptions { export interface ResourcerOptions {
prefix?: string; prefix?: string;
@ -84,6 +85,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
public readonly acl: ACL; public readonly acl: ACL;
public readonly errorHandler: ErrorHandler;
protected plugins = new Map<string, Plugin>(); protected plugins = new Map<string, Plugin>();
public listenServer: Server; public listenServer: Server;
@ -101,6 +104,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
app: this, app: this,
}); });
this.errorHandler = new ErrorHandler(this);
registerMiddlewares(this, options); registerMiddlewares(this, options);
if (options.registerActions !== false) { if (options.registerActions !== false) {
registerActions(this); registerActions(this);

View File

@ -0,0 +1,44 @@
import Application from './application';
export class ErrorHandler {
handlers = [];
constructor(app: Application) {}
register(guard: (err) => boolean, render: (err, ctx) => void) {
this.handlers.push({
guard,
render,
});
}
defaultHandler(err, ctx) {
ctx.status = err.statusCode || err.status || 500;
ctx.body = {
errors: [
{
message: err.message,
code: err.code,
},
],
};
}
middleware() {
const self = this;
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
for (const handler of self.handlers) {
if (handler.guard(err)) {
return handler.render(err, ctx);
}
}
self.defaultHandler(err, ctx);
}
};
}
}

View File

@ -9,6 +9,8 @@ import Application, { ApplicationOptions } from './application';
import { dataWrapping } from './middlewares/data-wrapping'; import { dataWrapping } from './middlewares/data-wrapping';
import { table2resource } from './middlewares/table2resource'; import { table2resource } from './middlewares/table2resource';
import ValidationError from 'sequelize';
export function createDatabase(options: ApplicationOptions) { export function createDatabase(options: ApplicationOptions) {
if (options.database instanceof Database) { if (options.database instanceof Database) {
return options.database; return options.database;
@ -93,7 +95,23 @@ export function createCli(app: Application, options: ApplicationOptions): Comman
return cli; return cli;
} }
function registerErrorHandler(app: Application) {
app.errorHandler.register(
(err) => err.name == 'SequelizeValidationError',
(err, ctx) => {
ctx.body = {
errors: err.errors.map((err) => ({ message: err.message })),
};
ctx.status = 400;
},
);
app.use(app.errorHandler.middleware());
}
export function registerMiddlewares(app: Application, options: ApplicationOptions) { export function registerMiddlewares(app: Application, options: ApplicationOptions) {
registerErrorHandler(app);
if (options.bodyParser !== false) { if (options.bodyParser !== false) {
app.use( app.use(
bodyParser({ bodyParser({