Skip to content

Commit 7ad4112

Browse files
committed
feat(event-handler): add function to convert Lambda proxy event to web response object
1 parent ea0e615 commit 7ad4112

File tree

7 files changed

+458
-28
lines changed

7 files changed

+458
-28
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from './errors.js';
2525
import { Route } from './Route.js';
2626
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
27+
import { isAPIGatewayProxyEvent } from './utils.js';
2728

2829
abstract class BaseRouter {
2930
protected context: Record<string, unknown>;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { APIGatewayProxyEvent } from 'aws-lambda';
2+
3+
function createBody(body: string | null, isBase64Encoded: boolean) {
4+
if (body === null) return null;
5+
6+
if (!isBase64Encoded) {
7+
return body;
8+
}
9+
return Buffer.from(body, 'base64').toString('utf8');
10+
}
11+
12+
export function proxyEventToWebRequest(event: APIGatewayProxyEvent) {
13+
const { httpMethod, path, domainName } = event.requestContext;
14+
15+
const headers = new Headers();
16+
for (const [name, value] of Object.entries(event.headers ?? {})) {
17+
if (value != null) headers.append(name, value);
18+
}
19+
20+
for (const [name, values] of Object.entries(event.multiValueHeaders ?? {})) {
21+
for (const value of values ?? []) {
22+
headers.append(name, value);
23+
}
24+
}
25+
const hostname = headers.get('Host') ?? domainName;
26+
const protocol = headers.get('X-Forwarded-Proto') ?? 'http';
27+
28+
const url = new URL(path, `${protocol}://${hostname}/`);
29+
30+
for (const [name, value] of Object.entries(
31+
event.queryStringParameters ?? {}
32+
)) {
33+
if (value != null) url.searchParams.append(name, value);
34+
}
35+
36+
for (const [name, values] of Object.entries(
37+
event.multiValueQueryStringParameters ?? {}
38+
)) {
39+
for (const value of values ?? []) {
40+
url.searchParams.append(name, value);
41+
}
42+
}
43+
return new Request(url.toString(), {
44+
method: httpMethod,
45+
headers,
46+
body: createBody(event.body, event.isBase64Encoded),
47+
});
48+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
2+
import type { APIGatewayProxyEvent } from 'aws-lambda';
13
import type { CompiledRoute, Path, ValidationResult } from '../types/rest.js';
24
import { PARAM_PATTERN, SAFE_CHARS, UNSAFE_CHARS } from './constants.js';
35

@@ -43,3 +45,26 @@ export function validatePathPattern(path: Path): ValidationResult {
4345
issues,
4446
};
4547
}
48+
49+
/**
50+
* Type guard to check if the provided event is an API Gateway Proxy event.
51+
*
52+
* We use this function to ensure that the event is an object and has the
53+
* required properties without adding a dependency.
54+
*
55+
* @param event - The incoming event to check
56+
*/
57+
export const isAPIGatewayProxyEvent = (
58+
event: unknown
59+
): event is APIGatewayProxyEvent => {
60+
if (!isRecord(event)) return false;
61+
return (
62+
isString(event.httpMethod) &&
63+
isString(event.path) &&
64+
isString(event.resource) &&
65+
isRecord(event.headers) &&
66+
isRecord(event.requestContext) &&
67+
typeof event.isBase64Encoded === 'boolean' &&
68+
(event.body === null || isString(event.body))
69+
);
70+
};

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ interface CompiledRoute {
5353

5454
type DynamicRoute = Route & CompiledRoute;
5555

56-
// biome-ignore lint/suspicious/noExplicitAny: we want to keep arguments and return types as any to accept any type of function
5756
type RouteHandler<
5857
TParams = Record<string, unknown>,
5958
TReturn = Response | JSONObject,

packages/event-handler/tests/unit/rest/BaseRouter.test.ts

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
33
import { beforeEach, describe, expect, it, vi } from 'vitest';
44
import { BaseRouter } from '../../../src/rest/BaseRouter.js';
55
import { HttpErrorCodes, HttpVerbs } from '../../../src/rest/constants.js';
6+
import { proxyEventToWebRequest } from '../../../src/rest/converters.js';
67
import {
78
BadRequestError,
89
InternalServerError,
910
MethodNotAllowedError,
1011
NotFoundError,
1112
} from '../../../src/rest/errors.js';
13+
import { isAPIGatewayProxyEvent } from '../../../src/rest/utils.js';
1214
import type {
1315
HttpMethod,
1416
Path,
@@ -43,39 +45,19 @@ describe('Class: BaseRouter', () => {
4345
this.logger.error('test error');
4446
}
4547

46-
#isEvent(obj: unknown): asserts obj is APIGatewayProxyEvent {
47-
if (
48-
typeof obj !== 'object' ||
49-
obj === null ||
50-
!('path' in obj) ||
51-
!('httpMethod' in obj) ||
52-
typeof (obj as any).path !== 'string' ||
53-
!(obj as any).path.startsWith('/') ||
54-
typeof (obj as any).httpMethod !== 'string' ||
55-
!Object.values(HttpVerbs).includes(
56-
(obj as any).httpMethod as HttpMethod
57-
)
58-
) {
59-
throw new Error('Invalid event object');
60-
}
61-
}
62-
6348
public async resolve(
6449
event: unknown,
6550
context: Context,
6651
options?: any
6752
): Promise<unknown> {
68-
this.#isEvent(event);
53+
if (!isAPIGatewayProxyEvent(event))
54+
throw new Error('not an API Gateway event!');
6955
const { httpMethod: method, path } = event;
7056
const route = this.routeRegistry.resolve(
7157
method as HttpMethod,
7258
path as Path
7359
);
74-
const request = new Request(`http://localhost${path}`, {
75-
method,
76-
headers: event.headers as Record<string, string>,
77-
body: event.body,
78-
});
60+
const request = proxyEventToWebRequest(event);
7961
try {
8062
if (route == null)
8163
throw new NotFoundError(`Route ${method} ${path} not found`);
@@ -838,9 +820,9 @@ describe('Class: BaseRouter', () => {
838820
statusCode: HttpErrorCodes.BAD_REQUEST,
839821
error: 'Bad Request',
840822
message: error.message,
841-
hasRequest: options.request instanceof Request,
842-
hasEvent: options.event === testEvent,
843-
hasContext: options.context === context,
823+
hasRequest: options?.request instanceof Request,
824+
hasEvent: options?.event === testEvent,
825+
hasContext: options?.context === context,
844826
}));
845827

846828
app.get('/test', () => {

0 commit comments

Comments
 (0)