Skip to content

Commit 9f3a276

Browse files
committed
feat(event-handler): Add support for HTTP APIs (API Gateway v2)
1 parent 32bc066 commit 9f3a276

File tree

14 files changed

+1368
-359
lines changed

14 files changed

+1368
-359
lines changed

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

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
getStringFromEnv,
77
isDevMode,
88
} from '@aws-lambda-powertools/commons/utils/env';
9-
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
9+
import type {
10+
APIGatewayProxyEvent,
11+
APIGatewayProxyEventV2,
12+
APIGatewayProxyResult,
13+
APIGatewayProxyStructuredResultV2,
14+
Context,
15+
} from 'aws-lambda';
1016
import type { HandlerResponse, ResolveOptions } from '../types/index.js';
1117
import type {
1218
ErrorConstructor,
@@ -24,15 +30,16 @@ import type {
2430
} from '../types/rest.js';
2531
import { HttpStatusCodes, HttpVerbs } from './constants.js';
2632
import {
27-
handlerResultToProxyResult,
2833
handlerResultToWebResponse,
2934
proxyEventToWebRequest,
30-
webHeadersToApiGatewayV1Headers,
35+
webHeadersToApiGatewayHeaders,
36+
webResponseToProxyResult,
3137
} from './converters.js';
3238
import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js';
3339
import {
3440
HttpError,
35-
InternalServerError,
41+
InvalidEventError,
42+
InvalidHttpMethodError,
3643
MethodNotAllowedError,
3744
NotFoundError,
3845
} from './errors.js';
@@ -41,9 +48,9 @@ import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
4148
import {
4249
composeMiddleware,
4350
HttpResponseStream,
44-
isAPIGatewayProxyEvent,
51+
isAPIGatewayProxyEventV1,
52+
isAPIGatewayProxyEventV2,
4553
isExtendedAPIGatewayProxyResult,
46-
isHttpMethod,
4754
resolvePrefixedPath,
4855
} from './utils.js';
4956

@@ -202,38 +209,45 @@ class Router {
202209
event: unknown,
203210
context: Context,
204211
options?: ResolveOptions
205-
): Promise<HandlerResponse> {
206-
if (!isAPIGatewayProxyEvent(event)) {
212+
): Promise<RequestContext> {
213+
if (!isAPIGatewayProxyEventV1(event) && !isAPIGatewayProxyEventV2(event)) {
207214
this.logger.error(
208215
'Received an event that is not compatible with this resolver'
209216
);
210-
throw new InternalServerError();
217+
throw new InvalidEventError();
211218
}
212219

213-
const method = event.requestContext.httpMethod.toUpperCase();
214-
if (!isHttpMethod(method)) {
215-
this.logger.error(`HTTP method ${method} is not supported.`);
216-
// We can't throw a MethodNotAllowedError outside the try block as it
217-
// will be converted to an internal server error by the API Gateway runtime
218-
return {
219-
statusCode: HttpStatusCodes.METHOD_NOT_ALLOWED,
220-
body: '',
221-
};
220+
let req: Request;
221+
try {
222+
req = proxyEventToWebRequest(event);
223+
} catch (err) {
224+
if (err instanceof InvalidHttpMethodError) {
225+
this.logger.error(err);
226+
// We can't throw a MethodNotAllowedError outside the try block as it
227+
// will be converted to an internal server error by the API Gateway runtime
228+
return {
229+
event,
230+
context,
231+
req: new Request('https://invalid'),
232+
res: new Response('', { status: HttpStatusCodes.METHOD_NOT_ALLOWED }),
233+
params: {},
234+
};
235+
}
236+
throw err;
222237
}
223238

224-
const req = proxyEventToWebRequest(event);
225-
226239
const requestContext: RequestContext = {
227240
event,
228241
context,
229242
req,
230243
// this response should be overwritten by the handler, if it isn't
231244
// it means something went wrong with the middleware chain
232-
res: new Response('', { status: 500 }),
245+
res: new Response('', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR }),
233246
params: {},
234247
};
235248

236249
try {
250+
const method = req.method as HttpMethod;
237251
const path = new URL(req.url).pathname as Path;
238252

239253
const route = this.routeRegistry.resolve(method, path);
@@ -255,6 +269,7 @@ class Router {
255269
: route.handler.bind(options.scope);
256270

257271
const handlerResult = await handler(reqCtx);
272+
258273
reqCtx.res = handlerResultToWebResponse(
259274
handlerResult,
260275
reqCtx.res.headers
@@ -277,13 +292,25 @@ class Router {
277292
});
278293

279294
// middleware result takes precedence to allow short-circuiting
280-
return middlewareResult ?? requestContext.res;
295+
if (middlewareResult !== undefined) {
296+
requestContext.res = handlerResultToWebResponse(
297+
middlewareResult,
298+
requestContext.res.headers
299+
);
300+
}
301+
302+
return requestContext;
281303
} catch (error) {
282304
this.logger.debug(`There was an error processing the request: ${error}`);
283-
return this.handleError(error as Error, {
305+
const res = await this.handleError(error as Error, {
284306
...requestContext,
285307
scope: options?.scope,
286308
});
309+
requestContext.res = handlerResultToWebResponse(
310+
res,
311+
requestContext.res.headers
312+
);
313+
return requestContext;
287314
}
288315
}
289316

@@ -296,15 +323,30 @@ class Router {
296323
* @param event - The Lambda event to resolve
297324
* @param context - The Lambda context
298325
* @param options - Optional resolve options for scope binding
299-
* @returns An API Gateway proxy result
326+
* @returns An API Gateway proxy result (V1 or V2 format depending on event version)
300327
*/
328+
public async resolve(
329+
event: APIGatewayProxyEvent,
330+
context: Context,
331+
options?: ResolveOptions
332+
): Promise<APIGatewayProxyResult>;
333+
public async resolve(
334+
event: APIGatewayProxyEventV2,
335+
context: Context,
336+
options?: ResolveOptions
337+
): Promise<APIGatewayProxyStructuredResultV2>;
301338
public async resolve(
302339
event: unknown,
303340
context: Context,
304341
options?: ResolveOptions
305-
): Promise<APIGatewayProxyResult> {
306-
const result = await this.#resolve(event, context, options);
307-
return handlerResultToProxyResult(result);
342+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2>;
343+
public async resolve(
344+
event: unknown,
345+
context: Context,
346+
options?: ResolveOptions
347+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2> {
348+
const reqCtx = await this.#resolve(event, context, options);
349+
return webResponseToProxyResult(reqCtx.res, reqCtx.event);
308350
}
309351

310352
/**
@@ -321,31 +363,33 @@ class Router {
321363
context: Context,
322364
options: ResolveStreamOptions
323365
): Promise<void> {
324-
const result = await this.#resolve(event, context, options);
325-
await this.#streamHandlerResponse(result, options.responseStream);
366+
const reqCtx = await this.#resolve(event, context, options);
367+
await this.#streamHandlerResponse(reqCtx, options.responseStream);
326368
}
327369

328370
/**
329371
* Streams a handler response to the Lambda response stream.
330372
* Converts the response to a web response and pipes it through the stream.
331373
*
332-
* @param response - The handler response to stream
374+
* @param reqCtx - The request context containing the response to stream
333375
* @param responseStream - The Lambda response stream to write to
334376
*/
335377
async #streamHandlerResponse(
336-
response: HandlerResponse,
378+
reqCtx: RequestContext,
337379
responseStream: ResponseStream
338380
) {
339-
const webResponse = handlerResultToWebResponse(response);
340-
const { headers } = webHeadersToApiGatewayV1Headers(webResponse.headers);
381+
const { headers } = webHeadersToApiGatewayHeaders(
382+
reqCtx.res.headers,
383+
reqCtx.event
384+
);
341385
const resStream = HttpResponseStream.from(responseStream, {
342-
statusCode: webResponse.status,
386+
statusCode: reqCtx.res.status,
343387
headers,
344388
});
345389

346-
if (webResponse.body) {
390+
if (reqCtx.res.body) {
347391
const nodeStream = Readable.fromWeb(
348-
webResponse.body as streamWeb.ReadableStream
392+
reqCtx.res.body as streamWeb.ReadableStream
349393
);
350394
await pipeline(nodeStream, resStream);
351395
} else {

0 commit comments

Comments
 (0)