Skip to content

Commit 562747a

Browse files
authored
feat(event-handler): add decorator functionality for error handlers (#4323)
1 parent 5bb23ad commit 562747a

File tree

2 files changed

+237
-11
lines changed

2 files changed

+237
-11
lines changed

packages/event-handler/src/rest/BaseRouter.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,68 @@ abstract class BaseRouter {
6868
public errorHandler<T extends Error>(
6969
errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
7070
handler: ErrorHandler<T>
71-
): void {
72-
this.errorHandlerRegistry.register(errorType, handler);
71+
): void;
72+
public errorHandler<T extends Error>(
73+
errorType: ErrorConstructor<T> | ErrorConstructor<T>[]
74+
): MethodDecorator;
75+
public errorHandler<T extends Error>(
76+
errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
77+
handler?: ErrorHandler<T>
78+
): MethodDecorator | undefined {
79+
if (handler && typeof handler === 'function') {
80+
this.errorHandlerRegistry.register(errorType, handler);
81+
return;
82+
}
83+
84+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
85+
this.errorHandlerRegistry.register(errorType, descriptor?.value);
86+
return descriptor;
87+
};
7388
}
7489

7590
/**
7691
* Registers a custom handler for 404 Not Found errors.
7792
*
7893
* @param handler - The error handler function for NotFoundError
7994
*/
80-
public notFound(handler: ErrorHandler<NotFoundError>): void {
81-
this.errorHandlerRegistry.register(NotFoundError, handler);
95+
public notFound(handler: ErrorHandler<NotFoundError>): void;
96+
public notFound(): MethodDecorator;
97+
public notFound(
98+
handler?: ErrorHandler<NotFoundError>
99+
): MethodDecorator | undefined {
100+
if (handler && typeof handler === 'function') {
101+
this.errorHandlerRegistry.register(NotFoundError, handler);
102+
return;
103+
}
104+
105+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
106+
this.errorHandlerRegistry.register(NotFoundError, descriptor?.value);
107+
return descriptor;
108+
};
82109
}
83110

84111
/**
85112
* Registers a custom handler for 405 Method Not Allowed errors.
86113
*
87114
* @param handler - The error handler function for MethodNotAllowedError
88115
*/
89-
public methodNotAllowed(handler: ErrorHandler<MethodNotAllowedError>): void {
90-
this.errorHandlerRegistry.register(MethodNotAllowedError, handler);
116+
public methodNotAllowed(handler: ErrorHandler<MethodNotAllowedError>): void;
117+
public methodNotAllowed(): MethodDecorator;
118+
public methodNotAllowed(
119+
handler?: ErrorHandler<MethodNotAllowedError>
120+
): MethodDecorator | undefined {
121+
if (handler && typeof handler === 'function') {
122+
this.errorHandlerRegistry.register(MethodNotAllowedError, handler);
123+
return;
124+
}
125+
126+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
127+
this.errorHandlerRegistry.register(
128+
MethodNotAllowedError,
129+
descriptor?.value
130+
);
131+
return descriptor;
132+
};
91133
}
92134

93135
public abstract resolve(
@@ -110,13 +152,17 @@ abstract class BaseRouter {
110152
* back to a default handler.
111153
*
112154
* @param error - The error to handle
155+
* @param options - Optional resolve options for scope binding
113156
* @returns A Response object with appropriate status code and error details
114157
*/
115-
protected async handleError(error: Error): Promise<Response> {
158+
protected async handleError(
159+
error: Error,
160+
options?: ResolveOptions
161+
): Promise<Response> {
116162
const handler = this.errorHandlerRegistry.resolve(error);
117163
if (handler !== null) {
118164
try {
119-
const body = await handler(error);
165+
const body = await handler.apply(options?.scope ?? this, [error]);
120166
return new Response(JSON.stringify(body), {
121167
status: body.statusCode,
122168
headers: { 'Content-Type': 'application/json' },

packages/event-handler/tests/unit/rest/BaseRouter.test.ts

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,20 @@ describe('Class: BaseRouter', () => {
4040
}
4141
}
4242

43-
public async resolve(event: unknown, context: Context): Promise<unknown> {
43+
public async resolve(
44+
event: unknown,
45+
context: Context,
46+
options?: any
47+
): Promise<unknown> {
4448
this.#isEvent(event);
4549
const { method, path } = event;
4650
const route = this.routeRegistry.resolve(method, path);
4751
try {
4852
if (route == null)
4953
throw new NotFoundError(`Route ${method} ${path} not found`);
50-
return route.handler(event, context);
54+
return await route.handler(event, context);
5155
} catch (error) {
52-
return await this.handleError(error as Error);
56+
return await this.handleError(error as Error, options);
5357
}
5458
}
5559
}
@@ -590,4 +594,180 @@ describe('Class: BaseRouter', () => {
590594
expect(result.headers.get('Content-Type')).toBe('application/json');
591595
});
592596
});
597+
598+
describe('decorators error handling', () => {
599+
it('works with errorHandler decorator', async () => {
600+
// Prepare
601+
const app = new TestResolver();
602+
603+
class Lambda {
604+
@app.errorHandler(BadRequestError)
605+
public async handleBadRequest(error: BadRequestError) {
606+
return {
607+
statusCode: HttpErrorCodes.BAD_REQUEST,
608+
error: 'Bad Request',
609+
message: `Decorated: ${error.message}`,
610+
};
611+
}
612+
613+
@app.get('/test')
614+
public async getTest() {
615+
throw new BadRequestError('test error');
616+
}
617+
618+
public async handler(event: unknown, context: Context) {
619+
return app.resolve(event, context);
620+
}
621+
}
622+
623+
const lambda = new Lambda();
624+
625+
// Act
626+
const result = (await lambda.handler(
627+
{ path: '/test', method: 'GET' },
628+
context
629+
)) as Response;
630+
631+
// Assess
632+
expect(result).toBeInstanceOf(Response);
633+
expect(result.status).toBe(HttpErrorCodes.BAD_REQUEST);
634+
expect(await result.text()).toBe(
635+
JSON.stringify({
636+
statusCode: HttpErrorCodes.BAD_REQUEST,
637+
error: 'Bad Request',
638+
message: 'Decorated: test error',
639+
})
640+
);
641+
});
642+
643+
it('works with notFound decorator', async () => {
644+
// Prepare
645+
const app = new TestResolver();
646+
647+
class Lambda {
648+
@app.notFound()
649+
public async handleNotFound(error: NotFoundError) {
650+
return {
651+
statusCode: HttpErrorCodes.NOT_FOUND,
652+
error: 'Not Found',
653+
message: `Decorated: ${error.message}`,
654+
};
655+
}
656+
657+
public async handler(event: unknown, context: Context) {
658+
return app.resolve(event, context);
659+
}
660+
}
661+
662+
const lambda = new Lambda();
663+
664+
// Act
665+
const result = (await lambda.handler(
666+
{ path: '/nonexistent', method: 'GET' },
667+
context
668+
)) as Response;
669+
670+
// Assess
671+
expect(result).toBeInstanceOf(Response);
672+
expect(result.status).toBe(HttpErrorCodes.NOT_FOUND);
673+
expect(await result.text()).toBe(
674+
JSON.stringify({
675+
statusCode: HttpErrorCodes.NOT_FOUND,
676+
error: 'Not Found',
677+
message: 'Decorated: Route GET /nonexistent not found',
678+
})
679+
);
680+
});
681+
682+
it('works with methodNotAllowed decorator', async () => {
683+
// Prepare
684+
const app = new TestResolver();
685+
686+
class Lambda {
687+
@app.methodNotAllowed()
688+
public async handleMethodNotAllowed(error: MethodNotAllowedError) {
689+
return {
690+
statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED,
691+
error: 'Method Not Allowed',
692+
message: `Decorated: ${error.message}`,
693+
};
694+
}
695+
696+
@app.get('/test')
697+
public async getTest() {
698+
throw new MethodNotAllowedError('POST not allowed');
699+
}
700+
701+
public async handler(event: unknown, context: Context) {
702+
return app.resolve(event, context);
703+
}
704+
}
705+
706+
const lambda = new Lambda();
707+
708+
// Act
709+
const result = (await lambda.handler(
710+
{ path: '/test', method: 'GET' },
711+
context
712+
)) as Response;
713+
714+
// Assess
715+
expect(result).toBeInstanceOf(Response);
716+
expect(result.status).toBe(HttpErrorCodes.METHOD_NOT_ALLOWED);
717+
expect(await result.text()).toBe(
718+
JSON.stringify({
719+
statusCode: HttpErrorCodes.METHOD_NOT_ALLOWED,
720+
error: 'Method Not Allowed',
721+
message: 'Decorated: POST not allowed',
722+
})
723+
);
724+
});
725+
726+
it('preserves scope when using error handler decorators', async () => {
727+
// Prepare
728+
const app = new TestResolver();
729+
730+
class Lambda {
731+
public scope = 'scoped';
732+
733+
@app.errorHandler(BadRequestError)
734+
public async handleBadRequest(error: BadRequestError) {
735+
return {
736+
statusCode: HttpErrorCodes.BAD_REQUEST,
737+
error: 'Bad Request',
738+
message: `${this.scope}: ${error.message}`,
739+
};
740+
}
741+
742+
@app.get('/test')
743+
public async getTest() {
744+
throw new BadRequestError('test error');
745+
}
746+
747+
public async handler(event: unknown, context: Context) {
748+
return app.resolve(event, context, { scope: this });
749+
}
750+
}
751+
752+
const lambda = new Lambda();
753+
const handler = lambda.handler.bind(lambda);
754+
755+
// Act
756+
const result = (await handler(
757+
{ path: '/test', method: 'GET' },
758+
context
759+
)) as Response;
760+
761+
// Assess
762+
expect(result).toBeInstanceOf(Response);
763+
expect(result.status).toBe(HttpErrorCodes.BAD_REQUEST);
764+
expect(await result.text()).toBe(
765+
JSON.stringify({
766+
statusCode: HttpErrorCodes.BAD_REQUEST,
767+
error: 'Bad Request',
768+
message: 'scoped: test error',
769+
})
770+
);
771+
});
772+
});
593773
});

0 commit comments

Comments
 (0)