Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions packages/event-handler/src/rest/BaseRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
getStringFromEnv,
isDevMode,
} from '@aws-lambda-powertools/commons/utils/env';
import type { Context } from 'aws-lambda';
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
import type { ResolveOptions } from '../types/index.js';
import type {
ErrorConstructor,
Expand All @@ -16,6 +16,11 @@ import type {
RouterOptions,
} from '../types/rest.js';
import { HttpVerbs } from './constants.js';
import {
handlerResultToProxyResult,
proxyEventToWebRequest,
responseToProxyResult,
} from './converters.js';
import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js';
import {
MethodNotAllowedError,
Expand All @@ -24,6 +29,7 @@ import {
} from './errors.js';
import { Route } from './Route.js';
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
import { isAPIGatewayProxyEvent } from './utils.js';

abstract class BaseRouter {
protected context: Record<string, unknown>;
Expand Down Expand Up @@ -133,11 +139,60 @@ abstract class BaseRouter {
};
}

public abstract resolve(
/**
* Resolves an API Gateway event by routing it to the appropriate handler
* and converting the result to an API Gateway proxy result. Handles errors
* using registered error handlers or falls back to default error handling
* (500 Internal Server Error).
*
* @param event - The Lambda event to resolve
* @param context - The Lambda context
* @param options - Optional resolve options for scope binding
* @returns An API Gateway proxy result or undefined for incompatible events
*/
public async resolve(
event: unknown,
context: Context,
options?: ResolveOptions
): Promise<unknown>;
): Promise<APIGatewayProxyResult | undefined> {
if (!isAPIGatewayProxyEvent(event)) {
this.logger.warn(
'Received an event that is not compatible with this resolver'
);
return;
}

try {
const request = proxyEventToWebRequest(event);
const path = new URL(request.url).pathname as Path;
const method = request.method.toUpperCase() as HttpMethod;

const route = this.routeRegistry.resolve(method, path);

if (route === null) {
throw new NotFoundError(`Route ${path} for method ${method} not found`);
}

const result = await route.handler.apply(options?.scope ?? this, [
route.params,
{
event,
context,
request,
},
]);

return await handlerResultToProxyResult(result);
} catch (error) {
const result = await this.handleError(error as Error, {
request: proxyEventToWebRequest(event),
event,
context,
scope: options?.scope,
});
return await responseToProxyResult(result);
}
}

public route(handler: RouteHandler, options: RouteOptions): void {
const { method, path } = options;
Expand Down
79 changes: 75 additions & 4 deletions packages/event-handler/src/rest/converters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { APIGatewayProxyEvent } from 'aws-lambda';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import type { HandlerResponse } from '../types/rest.js';
import { isAPIGatewayProxyResult } from './utils.js';

/**
* Creates a request body from API Gateway event body, handling base64 decoding if needed.
*
* @param body - The raw body from the API Gateway event
* @param isBase64Encoded - Whether the body is base64 encoded
* @returns The decoded body string or null
*/
const createBody = (body: string | null, isBase64Encoded: boolean) => {
if (body === null) return null;

Expand All @@ -9,7 +18,15 @@ const createBody = (body: string | null, isBase64Encoded: boolean) => {
return Buffer.from(body, 'base64').toString('utf8');
};

export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
/**
* Converts an API Gateway proxy event to a Web API Request object.
*
* @param event - The API Gateway proxy event
* @returns A Web API Request object
*/
export const proxyEventToWebRequest = (
event: APIGatewayProxyEvent
): Request => {
const { httpMethod, path, domainName } = event.requestContext;

const headers = new Headers();
Expand All @@ -23,7 +40,7 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
}
}
const hostname = headers.get('Host') ?? domainName;
const protocol = headers.get('X-Forwarded-Proto') ?? 'http';
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';

const url = new URL(path, `${protocol}://${hostname}/`);

Expand All @@ -45,4 +62,58 @@ export const proxyEventToWebRequest = (event: APIGatewayProxyEvent) => {
headers,
body: createBody(event.body, event.isBase64Encoded),
});
}
};

/**
* Converts a Web API Response object to an API Gateway proxy result.
*
* @param response - The Web API Response object
* @returns An API Gateway proxy result
*/
export const responseToProxyResult = async (
response: Response
): Promise<APIGatewayProxyResult> => {
const headers: Record<string, string> = {};
const multiValueHeaders: Record<string, Array<string>> = {};

for (const [key, value] of response.headers.entries()) {
const values = value.split(',').map((v) => v.trimStart());
if (values.length > 1) {
multiValueHeaders[key] = values;
} else {
headers[key] = value;
}
}

return {
statusCode: response.status,
headers,
multiValueHeaders,
body: await response.text(),
isBase64Encoded: false,
};
};

/**
* Converts a handler response to an API Gateway proxy result.
* Handles APIGatewayProxyResult, Response objects, and plain objects.
*
* @param response - The handler response (APIGatewayProxyResult, Response, or plain object)
* @returns An API Gateway proxy result
*/
export const handlerResultToProxyResult = async (
response: HandlerResponse
): Promise<APIGatewayProxyResult> => {
if (isAPIGatewayProxyResult(response)) {
return response;
}
if (response instanceof Response) {
return await responseToProxyResult(response);
}
return {
statusCode: 200,
body: JSON.stringify(response),
headers: { 'Content-Type': 'application/json' },
isBase64Encoded: false,
};
};
25 changes: 24 additions & 1 deletion packages/event-handler/src/rest/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
import type { APIGatewayProxyEvent } from 'aws-lambda';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import type { CompiledRoute, Path, ValidationResult } from '../types/rest.js';
import { PARAM_PATTERN, SAFE_CHARS, UNSAFE_CHARS } from './constants.js';

Expand Down Expand Up @@ -68,3 +68,26 @@ export const isAPIGatewayProxyEvent = (
(event.body === null || isString(event.body))
);
};

/**
* Type guard to check if the provided result is an API Gateway Proxy result.
*
* We use this function to ensure that the result is an object and has the
* required properties without adding a dependency.
*
* @param result - The result to check
*/
export const isAPIGatewayProxyResult = (
result: unknown
): result is APIGatewayProxyResult => {
if (!isRecord(result)) return false;
return (
typeof result.statusCode === 'number' &&
isString(result.body) &&
(result.headers === undefined || isRecord(result.headers)) &&
(result.multiValueHeaders === undefined ||
isRecord(result.multiValueHeaders)) &&
(result.isBase64Encoded === undefined ||
typeof result.isBase64Encoded === 'boolean')
);
};
5 changes: 4 additions & 1 deletion packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ interface CompiledRoute {

type DynamicRoute = Route & CompiledRoute;

type HandlerResponse = Response | JSONObject;

type RouteHandler<
TParams = Record<string, unknown>,
TReturn = Response | JSONObject,
TReturn = HandlerResponse,
> = (args: TParams, options?: RequestOptions) => Promise<TReturn>;

type HttpMethod = keyof typeof HttpVerbs;
Expand Down Expand Up @@ -106,6 +108,7 @@ export type {
ErrorHandlerRegistryOptions,
ErrorHandler,
ErrorResolveOptions,
HandlerResponse,
HttpStatusCode,
HttpMethod,
Path,
Expand Down
Loading
Loading