Skip to content

Commit e88ed90

Browse files
committed
feat(event-handler): add support for ALB
1 parent 8f487b9 commit e88ed90

File tree

10 files changed

+614
-40
lines changed

10 files changed

+614
-40
lines changed

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
isDevMode,
1212
} from '@aws-lambda-powertools/commons/utils/env';
1313
import type {
14+
ALBEvent,
15+
ALBResult,
1416
APIGatewayProxyEvent,
1517
APIGatewayProxyEventV2,
1618
APIGatewayProxyResult,
@@ -28,10 +30,10 @@ import type {
2830
RequestContext,
2931
ResolveStreamOptions,
3032
ResponseStream,
31-
ResponseType,
3233
RestRouteOptions,
3334
RestRouterOptions,
3435
RouteHandler,
36+
RouterResponse,
3537
} from '../types/rest.js';
3638
import { HttpStatusCodes, HttpVerbs } from './constants.js';
3739
import {
@@ -54,8 +56,10 @@ import {
5456
composeMiddleware,
5557
getBase64EncodingFromHeaders,
5658
getBase64EncodingFromResult,
59+
getResponseType,
5760
getStatusCode,
5861
HttpResponseStream,
62+
isALBEvent,
5963
isAPIGatewayProxyEventV1,
6064
isAPIGatewayProxyEventV2,
6165
isBinaryResult,
@@ -219,16 +223,18 @@ class Router {
219223
context: Context,
220224
options?: ResolveOptions
221225
): Promise<RequestContext> {
222-
if (!isAPIGatewayProxyEventV1(event) && !isAPIGatewayProxyEventV2(event)) {
226+
if (
227+
!isAPIGatewayProxyEventV1(event) &&
228+
!isAPIGatewayProxyEventV2(event) &&
229+
!isALBEvent(event)
230+
) {
223231
this.logger.error(
224232
'Received an event that is not compatible with this resolver'
225233
);
226234
throw new InvalidEventError();
227235
}
228236

229-
const responseType: ResponseType = isAPIGatewayProxyEventV2(event)
230-
? 'ApiGatewayV2'
231-
: 'ApiGatewayV1';
237+
const responseType = getResponseType(event);
232238

233239
let req: Request;
234240
try {
@@ -357,16 +363,21 @@ class Router {
357363
context: Context,
358364
options?: ResolveOptions
359365
): Promise<APIGatewayProxyStructuredResultV2>;
366+
public async resolve(
367+
event: ALBEvent,
368+
context: Context,
369+
options?: ResolveOptions
370+
): Promise<ALBResult>;
360371
public async resolve(
361372
event: unknown,
362373
context: Context,
363374
options?: ResolveOptions
364-
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2>;
375+
): Promise<RouterResponse>;
365376
public async resolve(
366377
event: unknown,
367378
context: Context,
368379
options?: ResolveOptions
369-
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2> {
380+
): Promise<RouterResponse> {
370381
const reqCtx = await this.#resolve(event, context, options);
371382
const isBase64Encoded =
372383
reqCtx.isBase64Encoded ??

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,78 @@ const COMPRESSION_ENCODING_TYPES = {
116116
ANY: '*',
117117
} as const;
118118

119+
const HttpStatusText: Record<number, string> = {
120+
// 2xx Success
121+
200: 'OK',
122+
201: 'Created',
123+
202: 'Accepted',
124+
203: 'Non-Authoritative Information',
125+
204: 'No Content',
126+
205: 'Reset Content',
127+
206: 'Partial Content',
128+
207: 'Multi-Status',
129+
208: 'Already Reported',
130+
226: 'IM Used',
131+
132+
// 3xx Redirection
133+
300: 'Multiple Choices',
134+
301: 'Moved Permanently',
135+
302: 'Found',
136+
303: 'See Other',
137+
304: 'Not Modified',
138+
305: 'Use Proxy',
139+
307: 'Temporary Redirect',
140+
308: 'Permanent Redirect',
141+
142+
// 4xx Client Error
143+
400: 'Bad Request',
144+
401: 'Unauthorized',
145+
402: 'Payment Required',
146+
403: 'Forbidden',
147+
404: 'Not Found',
148+
405: 'Method Not Allowed',
149+
406: 'Not Acceptable',
150+
407: 'Proxy Authentication Required',
151+
408: 'Request Timeout',
152+
409: 'Conflict',
153+
410: 'Gone',
154+
411: 'Length Required',
155+
412: 'Precondition Failed',
156+
413: 'Request Entity Too Large',
157+
414: 'Request-URI Too Long',
158+
415: 'Unsupported Media Type',
159+
416: 'Requested Range Not Satisfiable',
160+
417: 'Expectation Failed',
161+
418: "I'm a Teapot",
162+
421: 'Misdirected Request',
163+
422: 'Unprocessable Entity',
164+
423: 'Locked',
165+
424: 'Failed Dependency',
166+
425: 'Too Early',
167+
426: 'Upgrade Required',
168+
428: 'Precondition Required',
169+
429: 'Too Many Requests',
170+
431: 'Request Header Fields Too Large',
171+
451: 'Unavailable For Legal Reasons',
172+
173+
// 5xx Server Error
174+
500: 'Internal Server Error',
175+
501: 'Not Implemented',
176+
502: 'Bad Gateway',
177+
503: 'Service Unavailable',
178+
504: 'Gateway Timeout',
179+
505: 'HTTP Version Not Supported',
180+
506: 'Variant Also Negotiates',
181+
507: 'Insufficient Storage',
182+
508: 'Loop Detected',
183+
510: 'Not Extended',
184+
511: 'Network Authentication Required',
185+
};
186+
119187
export {
120188
HttpVerbs,
121189
HttpStatusCodes,
190+
HttpStatusText,
122191
PARAM_PATTERN,
123192
SAFE_CHARS,
124193
UNSAFE_CHARS,

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

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Readable } from 'node:stream';
22
import type streamWeb from 'node:stream/web';
33
import type {
4+
ALBEvent,
5+
ALBResult,
46
APIGatewayProxyEvent,
57
APIGatewayProxyEventV2,
68
APIGatewayProxyResult,
@@ -16,9 +18,10 @@ import type {
1618
V1Headers,
1719
WebResponseToProxyResultOptions,
1820
} from '../types/rest.js';
19-
import { HttpStatusCodes } from './constants.js';
21+
import { HttpStatusCodes, HttpStatusText } from './constants.js';
2022
import { InvalidHttpMethodError } from './errors.js';
2123
import {
24+
isALBEvent,
2225
isAPIGatewayProxyEventV2,
2326
isBinaryResult,
2427
isExtendedAPIGatewayProxyResult,
@@ -47,11 +50,11 @@ const createBody = (body: string | null, isBase64Encoded: boolean) => {
4750
* Populates headers from single and multi-value header entries.
4851
*
4952
* @param headers - The Headers object to populate
50-
* @param event - The API Gateway proxy event
53+
* @param event - The API Gateway proxy event or ALB event
5154
*/
5255
const populateV1Headers = (
5356
headers: Headers,
54-
event: APIGatewayProxyEvent
57+
event: APIGatewayProxyEvent | ALBEvent
5558
): void => {
5659
for (const [name, value] of Object.entries(event.headers ?? {})) {
5760
if (value !== undefined) headers.set(name, value);
@@ -71,9 +74,12 @@ const populateV1Headers = (
7174
* Populates URL search parameters from single and multi-value query string parameters.
7275
*
7376
* @param url - The URL object to populate
74-
* @param event - The API Gateway proxy event
77+
* @param event - The API Gateway proxy event or ALB event
7578
*/
76-
const populateV1QueryParams = (url: URL, event: APIGatewayProxyEvent): void => {
79+
const populateV1QueryParams = (
80+
url: URL,
81+
event: APIGatewayProxyEvent | ALBEvent
82+
): void => {
7783
for (const [name, value] of Object.entries(
7884
event.queryStringParameters ?? {}
7985
)) {
@@ -154,14 +160,39 @@ const proxyEventV2ToWebRequest = (event: APIGatewayProxyEventV2): Request => {
154160
};
155161

156162
/**
157-
* Converts an API Gateway proxy event (V1 or V2) to a Web API Request object.
163+
* Converts an ALB event to a Web API Request object.
164+
*
165+
* @param event - The ALB event
166+
* @returns A Web API Request object
167+
*/
168+
const albEventToWebRequest = (event: ALBEvent): Request => {
169+
const { httpMethod, path } = event;
170+
171+
const headers = new Headers();
172+
populateV1Headers(headers, event);
173+
174+
const hostname = headers.get('Host') ?? 'localhost';
175+
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';
176+
177+
const url = new URL(path, `${protocol}://${hostname}/`);
178+
populateV1QueryParams(url, event);
179+
180+
return new Request(url.toString(), {
181+
method: httpMethod,
182+
headers,
183+
body: createBody(event.body, event.isBase64Encoded),
184+
});
185+
};
186+
187+
/**
188+
* Converts an API Gateway proxy event (V1 or V2) or ALB event to a Web API Request object.
158189
* Automatically detects the event version and calls the appropriate converter.
159190
*
160-
* @param event - The API Gateway proxy event (V1 or V2)
191+
* @param event - The API Gateway proxy event (V1 or V2) or ALB event
161192
* @returns A Web API Request object
162193
*/
163194
const proxyEventToWebRequest = (
164-
event: APIGatewayProxyEvent | APIGatewayProxyEventV2
195+
event: APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBEvent
165196
): Request => {
166197
if (isAPIGatewayProxyEventV2(event)) {
167198
const method = event.requestContext.http.method.toUpperCase();
@@ -170,10 +201,13 @@ const proxyEventToWebRequest = (
170201
}
171202
return proxyEventV2ToWebRequest(event);
172203
}
173-
const method = event.requestContext.httpMethod.toUpperCase();
204+
const method = event.httpMethod.toUpperCase();
174205
if (!isHttpMethod(method)) {
175206
throw new InvalidHttpMethodError(method);
176207
}
208+
if (isALBEvent(event)) {
209+
return albEventToWebRequest(event);
210+
}
177211
return proxyEventV1ToWebRequest(event);
178212
};
179213

@@ -319,6 +353,42 @@ const webResponseToProxyResultV2 = async (
319353
return result;
320354
};
321355

356+
/**
357+
* Converts a Web API Response object to an ALB result.
358+
*
359+
* @param response - The Web API Response object
360+
* @param isBase64Encoded - Whether the response body should be base64 encoded (e.g., for binary or compressed content)
361+
* @returns An ALB result
362+
*/
363+
const webResponseToALBResult = async (
364+
response: Response,
365+
isBase64Encoded?: boolean
366+
): Promise<ALBResult> => {
367+
const { headers, multiValueHeaders } = webHeadersToApiGatewayV1Headers(
368+
response.headers
369+
);
370+
371+
const body = isBase64Encoded
372+
? await responseBodyToBase64(response)
373+
: await response.text();
374+
375+
const statusText = response.statusText || HttpStatusText[response.status];
376+
377+
const result: ALBResult = {
378+
statusCode: response.status,
379+
statusDescription: `${response.status} ${statusText}`,
380+
headers,
381+
body,
382+
isBase64Encoded,
383+
};
384+
385+
if (Object.keys(multiValueHeaders).length > 0) {
386+
result.multiValueHeaders = multiValueHeaders;
387+
}
388+
389+
return result;
390+
};
391+
322392
const webResponseToProxyResult = <T extends ResponseType>(
323393
response: Response,
324394
responseType: T,
@@ -330,6 +400,11 @@ const webResponseToProxyResult = <T extends ResponseType>(
330400
ResponseTypeMap[T]
331401
>;
332402
}
403+
if (responseType === 'ALB') {
404+
return webResponseToALBResult(response, isBase64Encoded) as Promise<
405+
ResponseTypeMap[T]
406+
>;
407+
}
333408
return webResponseToProxyResultV2(response, isBase64Encoded) as Promise<
334409
ResponseTypeMap[T]
335410
>;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export {
2121
export { Router } from './Router.js';
2222
export {
2323
composeMiddleware,
24+
isALBEvent,
2425
isAPIGatewayProxyEventV1,
2526
isAPIGatewayProxyEventV2,
2627
isExtendedAPIGatewayProxyResult,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isString,
66
} from '@aws-lambda-powertools/commons/typeutils';
77
import type {
8+
ALBEvent,
89
APIGatewayProxyEvent,
910
APIGatewayProxyEventV2,
1011
StreamifyHandler,
@@ -21,6 +22,7 @@ import type {
2122
Middleware,
2223
Path,
2324
ResponseStream,
25+
ResponseType,
2426
ValidationResult,
2527
} from '../types/rest.js';
2628
import {
@@ -140,6 +142,25 @@ export const isAPIGatewayProxyEventV2 = (
140142
);
141143
};
142144

145+
/**
146+
* Type guard to check if the provided event is an ALB event.
147+
*
148+
* @param event - The incoming event to check
149+
*/
150+
export const isALBEvent = (event: unknown): event is ALBEvent => {
151+
if (!isRecord(event)) return false;
152+
if (!isRecord(event.requestContext)) return false;
153+
return isRecord(event.requestContext.elb);
154+
};
155+
156+
export const getResponseType = (
157+
event: APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBEvent
158+
): ResponseType => {
159+
if (isAPIGatewayProxyEventV2(event)) return 'ApiGatewayV2';
160+
if (isALBEvent(event)) return 'ALB';
161+
return 'ApiGatewayV1';
162+
};
163+
143164
export const isHttpMethod = (method: string): method is HttpMethod => {
144165
return Object.keys(HttpVerbs).includes(method);
145166
};

0 commit comments

Comments
 (0)