Skip to content

Commit aa81927

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

File tree

14 files changed

+1372
-359
lines changed

14 files changed

+1372
-359
lines changed

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

Lines changed: 78 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,43 @@ 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+
return {
227+
event,
228+
context,
229+
req: new Request('https://invalid'),
230+
res: new Response('', { status: HttpStatusCodes.METHOD_NOT_ALLOWED }),
231+
params: {},
232+
};
233+
}
234+
throw err;
222235
}
223236

224-
const req = proxyEventToWebRequest(event);
225-
226237
const requestContext: RequestContext = {
227238
event,
228239
context,
229240
req,
230241
// this response should be overwritten by the handler, if it isn't
231242
// it means something went wrong with the middleware chain
232-
res: new Response('', { status: 500 }),
243+
res: new Response('', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR }),
233244
params: {},
234245
};
235246

236247
try {
248+
const method = req.method as HttpMethod;
237249
const path = new URL(req.url).pathname as Path;
238250

239251
const route = this.routeRegistry.resolve(method, path);
@@ -255,6 +267,7 @@ class Router {
255267
: route.handler.bind(options.scope);
256268

257269
const handlerResult = await handler(reqCtx);
270+
258271
reqCtx.res = handlerResultToWebResponse(
259272
handlerResult,
260273
reqCtx.res.headers
@@ -277,13 +290,25 @@ class Router {
277290
});
278291

279292
// middleware result takes precedence to allow short-circuiting
280-
return middlewareResult ?? requestContext.res;
293+
if (middlewareResult !== undefined) {
294+
requestContext.res = handlerResultToWebResponse(
295+
middlewareResult,
296+
requestContext.res.headers
297+
);
298+
}
299+
300+
return requestContext;
281301
} catch (error) {
282302
this.logger.debug(`There was an error processing the request: ${error}`);
283-
return this.handleError(error as Error, {
303+
const res = await this.handleError(error as Error, {
284304
...requestContext,
285305
scope: options?.scope,
286306
});
307+
requestContext.res = handlerResultToWebResponse(
308+
res,
309+
requestContext.res.headers
310+
);
311+
return requestContext;
287312
}
288313
}
289314

@@ -296,15 +321,30 @@ class Router {
296321
* @param event - The Lambda event to resolve
297322
* @param context - The Lambda context
298323
* @param options - Optional resolve options for scope binding
299-
* @returns An API Gateway proxy result
324+
* @returns An API Gateway proxy result (V1 or V2 format depending on event version)
300325
*/
326+
public async resolve(
327+
event: APIGatewayProxyEvent,
328+
context: Context,
329+
options?: ResolveOptions
330+
): Promise<APIGatewayProxyResult>;
331+
public async resolve(
332+
event: APIGatewayProxyEventV2,
333+
context: Context,
334+
options?: ResolveOptions
335+
): Promise<APIGatewayProxyStructuredResultV2>;
301336
public async resolve(
302337
event: unknown,
303338
context: Context,
304339
options?: ResolveOptions
305-
): Promise<APIGatewayProxyResult> {
306-
const result = await this.#resolve(event, context, options);
307-
return handlerResultToProxyResult(result);
340+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2>;
341+
public async resolve(
342+
event: unknown,
343+
context: Context,
344+
options?: ResolveOptions
345+
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2> {
346+
const reqCtx = await this.#resolve(event, context, options);
347+
return webResponseToProxyResult(reqCtx.res, reqCtx.event);
308348
}
309349

310350
/**
@@ -321,31 +361,33 @@ class Router {
321361
context: Context,
322362
options: ResolveStreamOptions
323363
): Promise<void> {
324-
const result = await this.#resolve(event, context, options);
325-
await this.#streamHandlerResponse(result, options.responseStream);
364+
const reqCtx = await this.#resolve(event, context, options);
365+
await this.#streamHandlerResponse(reqCtx, options.responseStream);
326366
}
327367

328368
/**
329369
* Streams a handler response to the Lambda response stream.
330370
* Converts the response to a web response and pipes it through the stream.
331371
*
332-
* @param response - The handler response to stream
372+
* @param reqCtx - The request context containing the response to stream
333373
* @param responseStream - The Lambda response stream to write to
334374
*/
335375
async #streamHandlerResponse(
336-
response: HandlerResponse,
376+
reqCtx: RequestContext,
337377
responseStream: ResponseStream
338378
) {
339-
const webResponse = handlerResultToWebResponse(response);
340-
const { headers } = webHeadersToApiGatewayV1Headers(webResponse.headers);
379+
const { headers } = webHeadersToApiGatewayHeaders(
380+
reqCtx.res.headers,
381+
reqCtx.event
382+
);
341383
const resStream = HttpResponseStream.from(responseStream, {
342-
statusCode: webResponse.status,
384+
statusCode: reqCtx.res.status,
343385
headers,
344386
});
345387

346-
if (webResponse.body) {
388+
if (reqCtx.res.body) {
347389
const nodeStream = Readable.fromWeb(
348-
webResponse.body as streamWeb.ReadableStream
390+
reqCtx.res.body as streamWeb.ReadableStream
349391
);
350392
await pipeline(nodeStream, resStream);
351393
} else {

0 commit comments

Comments
 (0)