Skip to content

Commit 9151633

Browse files
committed
feat(event-handler): implement mechanism to manipulate response in middleware
1 parent e6ea674 commit 9151633

File tree

8 files changed

+495
-64
lines changed

8 files changed

+495
-64
lines changed

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

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ErrorConstructor,
1010
ErrorHandler,
1111
ErrorResolveOptions,
12+
HandlerOptions,
1213
HttpMethod,
1314
Middleware,
1415
Path,
@@ -19,6 +20,7 @@ import type {
1920
import { HttpErrorCodes, HttpVerbs } from './constants.js';
2021
import {
2122
handlerResultToProxyResult,
23+
handlerResultToResponse,
2224
proxyEventToWebRequest,
2325
responseToProxyResult,
2426
} from './converters.js';
@@ -209,6 +211,15 @@ abstract class BaseRouter {
209211

210212
const request = proxyEventToWebRequest(event);
211213

214+
const handlerOptions: HandlerOptions = {
215+
event,
216+
context,
217+
request,
218+
// this response should be overwritten by the handler, if it isn't
219+
// it means somthing went wrong with the middleware chain
220+
res: new Response('', { status: 500 }),
221+
};
222+
212223
try {
213224
const path = new URL(request.url).pathname as Path;
214225

@@ -223,34 +234,38 @@ abstract class BaseRouter {
223234
? route.handler.bind(options.scope)
224235
: route.handler;
225236

237+
const handlerMiddleware: Middleware = async (params, options, next) => {
238+
const handlerResult = await handler(params, options);
239+
const handlerResponse = handlerResultToResponse(
240+
handlerResult,
241+
options.res.headers
242+
);
243+
244+
options.res = handlerResponse;
245+
246+
await next();
247+
};
248+
226249
const middleware = composeMiddleware([
227250
...this.middleware,
228251
...route.middleware,
252+
handlerMiddleware,
229253
]);
230254

231-
const result = await middleware(
255+
const middlewareResult = await middleware(
232256
route.params,
233-
{
234-
event,
235-
context,
236-
request,
237-
},
238-
() => handler(route.params, { event, context, request })
257+
handlerOptions,
258+
() => Promise.resolve()
239259
);
240260

241-
// In practice this we never happen because the final 'middleware' is
242-
// the handler function that allways returns HandlerResponse. However, the
243-
// type signature of of NextFunction includes undefined so we need this for
244-
// the TS compiler
245-
if (result === undefined) throw new InternalServerError();
261+
// middleware result takes precedence to allow short-circuiting
262+
const result = middlewareResult ?? handlerOptions.res;
246263

247-
return await handlerResultToProxyResult(result);
264+
return handlerResultToProxyResult(result);
248265
} catch (error) {
249266
this.logger.debug(`There was an error processing the request: ${error}`);
250267
const result = await this.handleError(error as Error, {
251-
request,
252-
event,
253-
context,
268+
...handlerOptions,
254269
scope: options?.scope,
255270
});
256271
return await responseToProxyResult(result);
@@ -281,13 +296,10 @@ abstract class BaseRouter {
281296
const handler = this.errorHandlerRegistry.resolve(error);
282297
if (handler !== null) {
283298
try {
284-
const body = await handler.apply(options.scope ?? this, [
299+
const { scope, ...handlerOptions } = options;
300+
const body = await handler.apply(scope ?? this, [
285301
error,
286-
{
287-
request: options.request,
288-
event: options.event,
289-
context: options.context,
290-
},
302+
handlerOptions,
291303
]);
292304
return new Response(JSON.stringify(body), {
293305
status: body.statusCode,

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

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,62 @@ export const responseToProxyResult = async (
8989
}
9090
}
9191

92-
return {
92+
const result: APIGatewayProxyResult = {
9393
statusCode: response.status,
9494
headers,
95-
multiValueHeaders,
9695
body: await response.text(),
9796
isBase64Encoded: false,
9897
};
98+
99+
if (Object.keys(multiValueHeaders).length > 0) {
100+
result.multiValueHeaders = multiValueHeaders;
101+
}
102+
103+
return result;
104+
};
105+
106+
/**
107+
* Converts a handler response to a Web API Response object.
108+
* Handles APIGatewayProxyResult, Response objects, and plain objects.
109+
*
110+
* @param response - The handler response (APIGatewayProxyResult, Response, or plain object)
111+
* @param headers - Optional headers to be included in the response
112+
* @returns A Web API Response object
113+
*/
114+
export const handlerResultToResponse = (
115+
response: HandlerResponse,
116+
headers: Headers = new Headers()
117+
): Response => {
118+
if (response instanceof Response) {
119+
return response;
120+
}
121+
if (isAPIGatewayProxyResult(response)) {
122+
const responseHeaders = new Headers();
123+
124+
for (const [key, value] of Object.entries(response.headers ?? {})) {
125+
if (value != null) {
126+
responseHeaders.set(key, String(value));
127+
}
128+
}
129+
130+
for (const [key, values] of Object.entries(
131+
response.multiValueHeaders ?? {}
132+
)) {
133+
for (const value of values ?? []) {
134+
responseHeaders.append(key, String(value));
135+
}
136+
}
137+
138+
return new Response(response.body, {
139+
status: response.statusCode,
140+
headers: responseHeaders,
141+
});
142+
}
143+
headers.set('Content-Type', 'application/json');
144+
return new Response(JSON.stringify(response), {
145+
status: 200,
146+
headers,
147+
});
99148
};
100149

101150
/**
@@ -117,7 +166,7 @@ export const handlerResultToProxyResult = async (
117166
return {
118167
statusCode: 200,
119168
body: JSON.stringify(response),
120-
headers: { 'Content-Type': 'application/json' },
169+
headers: { 'content-type': 'application/json' },
121170
isBase64Encoded: false,
122171
};
123172
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
22
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
33
import type {
44
CompiledRoute,
5+
HandlerOptions,
56
HandlerResponse,
67
HttpMethod,
78
Middleware,
89
Path,
9-
RequestOptions,
1010
ValidationResult,
1111
} from '../types/rest.js';
1212
import {
@@ -146,7 +146,7 @@ export const isAPIGatewayProxyResult = (
146146
export const composeMiddleware = (middleware: Middleware[]): Middleware => {
147147
return async (
148148
params: Record<string, string>,
149-
options: RequestOptions,
149+
options: HandlerOptions,
150150
next: () => Promise<HandlerResponse | void>
151151
): Promise<HandlerResponse | void> => {
152152
let index = -1;

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import type {
22
GenericLogger,
33
JSONObject,
44
} from '@aws-lambda-powertools/commons/types';
5-
import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
5+
import type {
6+
APIGatewayProxyEvent,
7+
APIGatewayProxyResult,
8+
Context,
9+
} from 'aws-lambda';
610
import type { BaseRouter } from '../rest/BaseRouter.js';
711
import type { HttpErrorCodes, HttpVerbs } from '../rest/constants.js';
812
import type { Route } from '../rest/Route.js';
@@ -14,17 +18,18 @@ type ErrorResponse = {
1418
message: string;
1519
};
1620

17-
type RequestOptions = {
21+
type HandlerOptions = {
1822
request: Request;
1923
event: APIGatewayProxyEvent;
2024
context: Context;
25+
res: Response;
2126
};
2227

23-
type ErrorResolveOptions = RequestOptions & ResolveOptions;
28+
type ErrorResolveOptions = HandlerOptions & ResolveOptions;
2429

2530
type ErrorHandler<T extends Error = Error> = (
2631
error: T,
27-
options: RequestOptions
32+
options: HandlerOptions
2833
) => Promise<ErrorResponse>;
2934

3035
interface ErrorConstructor<T extends Error = Error> {
@@ -58,7 +63,7 @@ type HandlerResponse = Response | JSONObject;
5863
type RouteHandler<
5964
TParams = Record<string, unknown>,
6065
TReturn = HandlerResponse,
61-
> = (args: TParams, options: RequestOptions) => Promise<TReturn>;
66+
> = (args: TParams, options: HandlerOptions) => Promise<TReturn>;
6267

6368
type HttpMethod = keyof typeof HttpVerbs;
6469

@@ -83,7 +88,7 @@ type NextFunction = () => Promise<HandlerResponse | void>;
8388

8489
type Middleware = (
8590
params: Record<string, string>,
86-
options: RequestOptions,
91+
options: HandlerOptions,
8792
next: NextFunction
8893
) => Promise<void | HandlerResponse>;
8994

@@ -123,7 +128,7 @@ export type {
123128
HttpMethod,
124129
Middleware,
125130
Path,
126-
RequestOptions,
131+
HandlerOptions,
127132
RouterOptions,
128133
RouteHandler,
129134
RouteOptions,

0 commit comments

Comments
 (0)