Skip to content

Commit 49d5f8a

Browse files
authored
fix(event-handler): run global middleware on all requests for REST API (#4507)
1 parent 972cd1f commit 49d5f8a

File tree

5 files changed

+104
-33
lines changed

5 files changed

+104
-33
lines changed

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,33 +226,40 @@ class Router {
226226

227227
const route = this.routeRegistry.resolve(method, path);
228228

229-
if (route === null) {
230-
throw new NotFoundError(`Route ${path} for method ${method} not found`);
231-
}
232-
233-
const handler =
234-
options?.scope != null
235-
? route.handler.bind(options.scope)
236-
: route.handler;
237-
238229
const handlerMiddleware: Middleware = async (params, reqCtx, next) => {
239-
const handlerResult = await handler(params, reqCtx);
240-
reqCtx.res = handlerResultToWebResponse(
241-
handlerResult,
242-
reqCtx.res.headers
243-
);
230+
if (route === null) {
231+
const notFoundRes = await this.handleError(
232+
new NotFoundError(`Route ${path} for method ${method} not found`),
233+
{ ...reqCtx, scope: options?.scope }
234+
);
235+
reqCtx.res = handlerResultToWebResponse(
236+
notFoundRes,
237+
reqCtx.res.headers
238+
);
239+
} else {
240+
const handler =
241+
options?.scope != null
242+
? route.handler.bind(options.scope)
243+
: route.handler;
244+
245+
const handlerResult = await handler(params, reqCtx);
246+
reqCtx.res = handlerResultToWebResponse(
247+
handlerResult,
248+
reqCtx.res.headers
249+
);
250+
}
244251

245252
await next();
246253
};
247254

248255
const middleware = composeMiddleware([
249256
...this.middleware,
250-
...route.middleware,
257+
...(route?.middleware ?? []),
251258
handlerMiddleware,
252259
]);
253260

254261
const middlewareResult = await middleware(
255-
route.params,
262+
route?.params ?? {},
256263
requestContext,
257264
() => Promise.resolve()
258265
);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,14 @@ export const handlerResultToWebResponse = (
140140
resHeaders?: Headers
141141
): Response => {
142142
if (response instanceof Response) {
143-
return response;
143+
const headers = new Headers(resHeaders);
144+
for (const [key, value] of response.headers.entries()) {
145+
headers.set(key, value);
146+
}
147+
return new Response(response.body, {
148+
status: response.status,
149+
headers,
150+
});
144151
}
145152

146153
const headers = new Headers(resHeaders);

packages/event-handler/tests/unit/rest/Router/decorators.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,29 +256,32 @@ describe('Class: Router - Decorators', () => {
256256
});
257257
});
258258

259-
it('works with notFound decorator', async () => {
259+
it('works with notFound decorator and preserves scope', async () => {
260260
// Prepare
261261
const app = new Router();
262262

263263
class Lambda {
264+
public scope = 'scoped';
265+
264266
@app.notFound()
265267
public async handleNotFound(error: NotFoundError) {
266268
return {
267269
statusCode: HttpErrorCodes.NOT_FOUND,
268270
error: 'Not Found',
269-
message: `Decorated: ${error.message}`,
271+
message: `${this.scope}: ${error.message}`,
270272
};
271273
}
272274

273275
public async handler(event: unknown, _context: Context) {
274-
return app.resolve(event, _context);
276+
return app.resolve(event, _context, { scope: this });
275277
}
276278
}
277279

278280
const lambda = new Lambda();
281+
const handler = lambda.handler.bind(lambda);
279282

280283
// Act
281-
const result = await lambda.handler(
284+
const result = await handler(
282285
createTestEvent('/nonexistent', 'GET'),
283286
context
284287
);
@@ -289,7 +292,7 @@ describe('Class: Router - Decorators', () => {
289292
body: JSON.stringify({
290293
statusCode: HttpErrorCodes.NOT_FOUND,
291294
error: 'Not Found',
292-
message: 'Decorated: Route /nonexistent for method GET not found',
295+
message: 'scoped: Route /nonexistent for method GET not found',
293296
}),
294297
headers: { 'content-type': 'application/json' },
295298
isBase64Encoded: false,

packages/event-handler/tests/unit/rest/Router/middleware.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,33 @@ describe('Class: Router - Middleware', () => {
421421
});
422422
});
423423

424+
it('headers set by handler before next() take precedence', async () => {
425+
// Prepare
426+
const app = new Router();
427+
428+
app.use(async (_params, reqCtx, next) => {
429+
reqCtx.res.headers.set('x-before-handler', 'middleware-value');
430+
await next();
431+
});
432+
433+
app.get('/handler-precedence', async () => {
434+
const response = Response.json({ success: true });
435+
response.headers.set('x-before-handler', 'handler-value');
436+
return response;
437+
});
438+
439+
// Act
440+
const result = await app.resolve(
441+
createTestEvent('/handler-precedence', 'GET'),
442+
context
443+
);
444+
445+
// Assess
446+
expect(result.statusCode).toBe(200);
447+
expect(result.headers?.['content-type']).toBe('application/json');
448+
expect(result.headers?.['x-before-handler']).toBe('handler-value');
449+
});
450+
424451
it('overwrites headers when set later in the middleware stack', async () => {
425452
// Prepare
426453
const app = new Router();
@@ -455,6 +482,24 @@ describe('Class: Router - Middleware', () => {
455482
});
456483
});
457484

485+
it('executes global middleware even when route is not found', async () => {
486+
// Prepare
487+
const app = new Router();
488+
const executionOrder: string[] = [];
489+
490+
const globalMiddleware = createTrackingMiddleware(
491+
'global',
492+
executionOrder
493+
);
494+
app.use(globalMiddleware);
495+
496+
// Act
497+
await app.resolve(createTestEvent('/nonexistent', 'GET'), context);
498+
499+
// Assess
500+
expect(executionOrder).toEqual(['global-start', 'global-end']);
501+
});
502+
458503
it('works with class decorators and preserves scope access', async () => {
459504
// Prepare
460505
const app = new Router();

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -549,17 +549,6 @@ describe('Converters', () => {
549549
});
550550

551551
describe('handlerResultToResponse', () => {
552-
it('returns Response object as-is', () => {
553-
// Prepare
554-
const response = new Response('Hello', { status: 201 });
555-
556-
// Act
557-
const result = handlerResultToWebResponse(response);
558-
559-
// Assess
560-
expect(result).toBe(response);
561-
});
562-
563552
it('converts APIGatewayProxyResult to Response', async () => {
564553
// Prepare
565554
const proxyResult = {
@@ -678,5 +667,25 @@ describe('Converters', () => {
678667
// Assess
679668
expect(result.headers.get('content-type')).toBe('text/plain');
680669
});
670+
671+
it('merges headers from resHeaders with Response object, headers from Response take precedence', () => {
672+
// Prepare
673+
const response = new Response('Hello', {
674+
headers: { 'content-type': 'text/plain' },
675+
});
676+
const resHeaders = new Headers({
677+
'x-custom': 'value',
678+
'content-type': 'application/json', // should not override existing
679+
});
680+
681+
// Act
682+
const result = handlerResultToWebResponse(response, resHeaders);
683+
684+
// Assess
685+
expect(result.headers.get('content-type')).toBe('text/plain');
686+
expect(result.headers.get('x-custom')).toBe('value');
687+
expect(result.status).toBe(200);
688+
expect(result.text()).resolves.toBe('Hello');
689+
});
681690
});
682691
});

0 commit comments

Comments
 (0)