Skip to content

Commit 2f70018

Browse files
authored
feat(event-handler): Add support for HTTP APIs (API Gateway v2) (#4714)
1 parent b387db0 commit 2f70018

File tree

14 files changed

+1357
-359
lines changed

14 files changed

+1357
-359
lines changed

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

Lines changed: 87 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,
@@ -18,21 +24,23 @@ import type {
1824
RequestContext,
1925
ResolveStreamOptions,
2026
ResponseStream,
27+
ResponseType,
2128
RestRouteOptions,
2229
RestRouterOptions,
2330
RouteHandler,
2431
} from '../types/rest.js';
2532
import { HttpStatusCodes, HttpVerbs } from './constants.js';
2633
import {
27-
handlerResultToProxyResult,
2834
handlerResultToWebResponse,
2935
proxyEventToWebRequest,
30-
webHeadersToApiGatewayV1Headers,
36+
webHeadersToApiGatewayHeaders,
37+
webResponseToProxyResult,
3138
} from './converters.js';
3239
import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js';
3340
import {
3441
HttpError,
35-
InternalServerError,
42+
InvalidEventError,
43+
InvalidHttpMethodError,
3644
MethodNotAllowedError,
3745
NotFoundError,
3846
} from './errors.js';
@@ -41,9 +49,9 @@ import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
4149
import {
4250
composeMiddleware,
4351
HttpResponseStream,
44-
isAPIGatewayProxyEvent,
52+
isAPIGatewayProxyEventV1,
53+
isAPIGatewayProxyEventV2,
4554
isExtendedAPIGatewayProxyResult,
46-
isHttpMethod,
4755
resolvePrefixedPath,
4856
} from './utils.js';
4957

@@ -202,38 +210,51 @@ class Router {
202210
event: unknown,
203211
context: Context,
204212
options?: ResolveOptions
205-
): Promise<HandlerResponse> {
206-
if (!isAPIGatewayProxyEvent(event)) {
213+
): Promise<RequestContext> {
214+
if (!isAPIGatewayProxyEventV1(event) && !isAPIGatewayProxyEventV2(event)) {
207215
this.logger.error(
208216
'Received an event that is not compatible with this resolver'
209217
);
210-
throw new InternalServerError();
218+
throw new InvalidEventError();
211219
}
212220

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-
};
222-
}
221+
const responseType: ResponseType = isAPIGatewayProxyEventV2(event)
222+
? 'ApiGatewayV2'
223+
: 'ApiGatewayV1';
223224

224-
const req = proxyEventToWebRequest(event);
225+
let req: Request;
226+
try {
227+
req = proxyEventToWebRequest(event);
228+
} catch (err) {
229+
if (err instanceof InvalidHttpMethodError) {
230+
this.logger.error(err);
231+
// We can't throw a MethodNotAllowedError outside the try block as it
232+
// will be converted to an internal server error by the API Gateway runtime
233+
return {
234+
event,
235+
context,
236+
req: new Request('https://invalid'),
237+
res: new Response('', { status: HttpStatusCodes.METHOD_NOT_ALLOWED }),
238+
params: {},
239+
responseType,
240+
};
241+
}
242+
throw err;
243+
}
225244

226245
const requestContext: RequestContext = {
227246
event,
228247
context,
229248
req,
230249
// this response should be overwritten by the handler, if it isn't
231250
// it means something went wrong with the middleware chain
232-
res: new Response('', { status: 500 }),
251+
res: new Response('', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR }),
233252
params: {},
253+
responseType,
234254
};
235255

236256
try {
257+
const method = req.method as HttpMethod;
237258
const path = new URL(req.url).pathname as Path;
238259

239260
const route = this.routeRegistry.resolve(method, path);
@@ -255,6 +276,7 @@ class Router {
255276
: route.handler.bind(options.scope);
256277

257278
const handlerResult = await handler(reqCtx);
279+
258280
reqCtx.res = handlerResultToWebResponse(
259281
handlerResult,
260282
reqCtx.res.headers
@@ -277,13 +299,25 @@ class Router {
277299
});
278300

279301
// middleware result takes precedence to allow short-circuiting
280-
return middlewareResult ?? requestContext.res;
302+
if (middlewareResult !== undefined) {
303+
requestContext.res = handlerResultToWebResponse(
304+
middlewareResult,
305+
requestContext.res.headers
306+
);
307+
}
308+
309+
return requestContext;
281310
} catch (error) {
282311
this.logger.debug(`There was an error processing the request: ${error}`);
283-
return this.handleError(error as Error, {
312+
const res = await this.handleError(error as Error, {
284313
...requestContext,
285314
scope: options?.scope,
286315
});
316+
requestContext.res = handlerResultToWebResponse(
317+
res,
318+
requestContext.res.headers
319+
);
320+
return requestContext;
287321
}
288322
}
289323

@@ -296,15 +330,30 @@ class Router {
296330
* @param event - The Lambda event to resolve
297331
* @param context - The Lambda context
298332
* @param options - Optional resolve options for scope binding
299-
* @returns An API Gateway proxy result
333+
* @returns An API Gateway proxy result (V1 or V2 format depending on event version)
300334
*/
335+
public async resolve(
336+
event: APIGatewayProxyEvent,
337+
context: Context,
338+
options?: ResolveOptions
339+
): Promise<APIGatewayProxyResult>;
340+
public async resolve(
341+
event: APIGatewayProxyEventV2,
342+
context: Context,
343+
options?: ResolveOptions
344+
): Promise<APIGatewayProxyStructuredResultV2>;
301345
public async resolve(
302346
event: unknown,
303347
context: Context,
304348
options?: ResolveOptions
305-
): Promise<APIGatewayProxyResult> {
306-
const result = await this.#resolve(event, context, options);
307-
return handlerResultToProxyResult(result);
349+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2>;
350+
public async resolve(
351+
event: unknown,
352+
context: Context,
353+
options?: ResolveOptions
354+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2> {
355+
const reqCtx = await this.#resolve(event, context, options);
356+
return webResponseToProxyResult(reqCtx.res, reqCtx.responseType);
308357
}
309358

310359
/**
@@ -321,31 +370,33 @@ class Router {
321370
context: Context,
322371
options: ResolveStreamOptions
323372
): Promise<void> {
324-
const result = await this.#resolve(event, context, options);
325-
await this.#streamHandlerResponse(result, options.responseStream);
373+
const reqCtx = await this.#resolve(event, context, options);
374+
await this.#streamHandlerResponse(reqCtx, options.responseStream);
326375
}
327376

328377
/**
329378
* Streams a handler response to the Lambda response stream.
330379
* Converts the response to a web response and pipes it through the stream.
331380
*
332-
* @param response - The handler response to stream
381+
* @param reqCtx - The request context containing the response to stream
333382
* @param responseStream - The Lambda response stream to write to
334383
*/
335384
async #streamHandlerResponse(
336-
response: HandlerResponse,
385+
reqCtx: RequestContext,
337386
responseStream: ResponseStream
338387
) {
339-
const webResponse = handlerResultToWebResponse(response);
340-
const { headers } = webHeadersToApiGatewayV1Headers(webResponse.headers);
388+
const { headers } = webHeadersToApiGatewayHeaders(
389+
reqCtx.res.headers,
390+
reqCtx.responseType
391+
);
341392
const resStream = HttpResponseStream.from(responseStream, {
342-
statusCode: webResponse.status,
393+
statusCode: reqCtx.res.status,
343394
headers,
344395
});
345396

346-
if (webResponse.body) {
397+
if (reqCtx.res.body) {
347398
const nodeStream = Readable.fromWeb(
348-
webResponse.body as streamWeb.ReadableStream
399+
reqCtx.res.body as streamWeb.ReadableStream
349400
);
350401
await pipeline(nodeStream, resStream);
351402
} else {

0 commit comments

Comments
 (0)