Skip to content

Commit 35a510d

Browse files
authored
feat(event-handler): implement mechanism to manipulate response in middleware (#4439)
1 parent f9fadd6 commit 35a510d

File tree

8 files changed

+456
-65
lines changed

8 files changed

+456
-65
lines changed

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

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import type {
1212
HttpMethod,
1313
Middleware,
1414
Path,
15+
RequestContext,
1516
RouteHandler,
1617
RouteOptions,
1718
RouterOptions,
1819
} from '../types/rest.js';
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: RequestContext = {
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,36 @@ 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+
options.res = handlerResultToResponse(
240+
handlerResult,
241+
options.res.headers
242+
);
243+
244+
await next();
245+
};
246+
226247
const middleware = composeMiddleware([
227248
...this.middleware,
228249
...route.middleware,
250+
handlerMiddleware,
229251
]);
230252

231-
const result = await middleware(
253+
const middlewareResult = await middleware(
232254
route.params,
233-
{
234-
event,
235-
context,
236-
request,
237-
},
238-
() => handler(route.params, { event, context, request })
255+
handlerOptions,
256+
() => Promise.resolve()
239257
);
240258

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();
259+
// middleware result takes precedence to allow short-circuiting
260+
const result = middlewareResult ?? handlerOptions.res;
246261

247-
return await handlerResultToProxyResult(result);
262+
return handlerResultToProxyResult(result);
248263
} catch (error) {
249264
this.logger.debug(`There was an error processing the request: ${error}`);
250265
const result = await this.handleError(error as Error, {
251-
request,
252-
event,
253-
context,
266+
...handlerOptions,
254267
scope: options?.scope,
255268
});
256269
return await responseToProxyResult(result);
@@ -281,13 +294,10 @@ abstract class BaseRouter {
281294
const handler = this.errorHandlerRegistry.resolve(error);
282295
if (handler !== null) {
283296
try {
284-
const body = await handler.apply(options.scope ?? this, [
297+
const { scope, ...handlerOptions } = options;
298+
const body = await handler.apply(scope ?? this, [
285299
error,
286-
{
287-
request: options.request,
288-
event: options.event,
289-
context: options.context,
290-
},
300+
handlerOptions,
291301
]);
292302
return new Response(JSON.stringify(body), {
293303
status: body.statusCode,

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,60 @@ 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+
resHeaders?: Headers
117+
): Response => {
118+
if (response instanceof Response) {
119+
return response;
120+
}
121+
122+
const headers = new Headers(resHeaders);
123+
headers.set('Content-Type', 'application/json');
124+
125+
if (isAPIGatewayProxyResult(response)) {
126+
for (const [key, value] of Object.entries(response.headers ?? {})) {
127+
if (value != null) {
128+
headers.set(key, String(value));
129+
}
130+
}
131+
132+
for (const [key, values] of Object.entries(
133+
response.multiValueHeaders ?? {}
134+
)) {
135+
for (const value of values ?? []) {
136+
headers.append(key, String(value));
137+
}
138+
}
139+
140+
return new Response(response.body, {
141+
status: response.statusCode,
142+
headers,
143+
});
144+
}
145+
return Response.json(response, { headers });
99146
};
100147

101148
/**
@@ -117,7 +164,7 @@ export const handlerResultToProxyResult = async (
117164
return {
118165
statusCode: 200,
119166
body: JSON.stringify(response),
120-
headers: { 'Content-Type': 'application/json' },
167+
headers: { 'content-type': 'application/json' },
121168
isBase64Encoded: false,
122169
};
123170
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
HttpMethod,
77
Middleware,
88
Path,
9-
RequestOptions,
9+
RequestContext,
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: RequestContext,
150150
next: () => Promise<HandlerResponse | void>
151151
): Promise<HandlerResponse | void> => {
152152
let index = -1;

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ type ErrorResponse = {
1414
message: string;
1515
};
1616

17-
type RequestOptions = {
17+
type RequestContext = {
1818
request: Request;
1919
event: APIGatewayProxyEvent;
2020
context: Context;
21+
res: Response;
2122
};
2223

23-
type ErrorResolveOptions = RequestOptions & ResolveOptions;
24+
type ErrorResolveOptions = RequestContext & ResolveOptions;
2425

2526
type ErrorHandler<T extends Error = Error> = (
2627
error: T,
27-
options: RequestOptions
28+
options: RequestContext
2829
) => Promise<ErrorResponse>;
2930

3031
interface ErrorConstructor<T extends Error = Error> {
@@ -58,7 +59,7 @@ type HandlerResponse = Response | JSONObject;
5859
type RouteHandler<
5960
TParams = Record<string, unknown>,
6061
TReturn = HandlerResponse,
61-
> = (args: TParams, options: RequestOptions) => Promise<TReturn>;
62+
> = (args: TParams, options: RequestContext) => Promise<TReturn>;
6263

6364
type HttpMethod = keyof typeof HttpVerbs;
6465

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

8485
type Middleware = (
8586
params: Record<string, string>,
86-
options: RequestOptions,
87+
options: RequestContext,
8788
next: NextFunction
8889
) => Promise<void | HandlerResponse>;
8990

@@ -123,7 +124,7 @@ export type {
123124
HttpMethod,
124125
Middleware,
125126
Path,
126-
RequestOptions,
127+
RequestContext,
127128
RouterOptions,
128129
RouteHandler,
129130
RouteOptions,

0 commit comments

Comments
 (0)