Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
120 changes: 84 additions & 36 deletions packages/event-handler/src/rest/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
getStringFromEnv,
isDevMode,
} from '@aws-lambda-powertools/commons/utils/env';
import type { APIGatewayProxyResult, Context } from 'aws-lambda';
import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyStructuredResultV2,
Context,
} from 'aws-lambda';
import type { HandlerResponse, ResolveOptions } from '../types/index.js';
import type {
ErrorConstructor,
Expand All @@ -24,15 +30,16 @@ import type {
} from '../types/rest.js';
import { HttpStatusCodes, HttpVerbs } from './constants.js';
import {
handlerResultToProxyResult,
handlerResultToWebResponse,
proxyEventToWebRequest,
webHeadersToApiGatewayV1Headers,
webHeadersToApiGatewayHeaders,
webResponseToProxyResult,
} from './converters.js';
import { ErrorHandlerRegistry } from './ErrorHandlerRegistry.js';
import {
HttpError,
InternalServerError,
InvalidEventError,
InvalidHttpMethodError,
MethodNotAllowedError,
NotFoundError,
} from './errors.js';
Expand All @@ -41,9 +48,9 @@ import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
import {
composeMiddleware,
HttpResponseStream,
isAPIGatewayProxyEvent,
isAPIGatewayProxyEventV1,
isAPIGatewayProxyEventV2,
isExtendedAPIGatewayProxyResult,
isHttpMethod,
resolvePrefixedPath,
} from './utils.js';

Expand Down Expand Up @@ -202,38 +209,49 @@ class Router {
event: unknown,
context: Context,
options?: ResolveOptions
): Promise<HandlerResponse> {
if (!isAPIGatewayProxyEvent(event)) {
): Promise<RequestContext> {
if (!isAPIGatewayProxyEventV1(event) && !isAPIGatewayProxyEventV2(event)) {
this.logger.error(
'Received an event that is not compatible with this resolver'
);
throw new InternalServerError();
throw new InvalidEventError();
}

const method = event.requestContext.httpMethod.toUpperCase();
if (!isHttpMethod(method)) {
this.logger.error(`HTTP method ${method} is not supported.`);
// We can't throw a MethodNotAllowedError outside the try block as it
// will be converted to an internal server error by the API Gateway runtime
return {
statusCode: HttpStatusCodes.METHOD_NOT_ALLOWED,
body: '',
};
}
const responseType = isAPIGatewayProxyEventV2(event) ? 'v2' : 'v1';

const req = proxyEventToWebRequest(event);
let req: Request;
try {
req = proxyEventToWebRequest(event);
} catch (err) {
if (err instanceof InvalidHttpMethodError) {
this.logger.error(err);
// We can't throw a MethodNotAllowedError outside the try block as it
// will be converted to an internal server error by the API Gateway runtime
return {
event,
context,
req: new Request('https://invalid'),
Copy link
Contributor Author

@svozza svozza Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fond of creating this dummy Request object but the alternative is to make req optional, which will make the ergonomics of writing middleware much worse if users need to do a null check any time they want to interact with a request that will always be there apart from this particular corner case.

res: new Response('', { status: HttpStatusCodes.METHOD_NOT_ALLOWED }),
params: {},
responseType,
};
}
throw err;
}

const requestContext: RequestContext = {
event,
context,
req,
// this response should be overwritten by the handler, if it isn't
// it means something went wrong with the middleware chain
res: new Response('', { status: 500 }),
res: new Response('', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR }),
params: {},
responseType,
};

try {
const method = req.method as HttpMethod;
const path = new URL(req.url).pathname as Path;

const route = this.routeRegistry.resolve(method, path);
Expand All @@ -255,6 +273,7 @@ class Router {
: route.handler.bind(options.scope);

const handlerResult = await handler(reqCtx);

reqCtx.res = handlerResultToWebResponse(
handlerResult,
reqCtx.res.headers
Expand All @@ -277,13 +296,25 @@ class Router {
});

// middleware result takes precedence to allow short-circuiting
return middlewareResult ?? requestContext.res;
if (middlewareResult !== undefined) {
requestContext.res = handlerResultToWebResponse(
middlewareResult,
requestContext.res.headers
);
}

return requestContext;
} catch (error) {
this.logger.debug(`There was an error processing the request: ${error}`);
return this.handleError(error as Error, {
const res = await this.handleError(error as Error, {
...requestContext,
scope: options?.scope,
});
requestContext.res = handlerResultToWebResponse(
res,
requestContext.res.headers
);
return requestContext;
}
}

Expand All @@ -296,15 +327,30 @@ class Router {
* @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
* @returns An API Gateway proxy result (V1 or V2 format depending on event version)
*/
public async resolve(
event: APIGatewayProxyEvent,
context: Context,
options?: ResolveOptions
): Promise<APIGatewayProxyResult>;
public async resolve(
event: APIGatewayProxyEventV2,
context: Context,
options?: ResolveOptions
): Promise<APIGatewayProxyStructuredResultV2>;
public async resolve(
event: unknown,
context: Context,
options?: ResolveOptions
): Promise<APIGatewayProxyResult> {
const result = await this.#resolve(event, context, options);
return handlerResultToProxyResult(result);
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2>;
public async resolve(
event: unknown,
context: Context,
options?: ResolveOptions
): Promise<APIGatewayProxyResult | APIGatewayProxyStructuredResultV2> {
const reqCtx = await this.#resolve(event, context, options);
return webResponseToProxyResult(reqCtx.res, reqCtx.responseType);
}

/**
Expand All @@ -321,31 +367,33 @@ class Router {
context: Context,
options: ResolveStreamOptions
): Promise<void> {
const result = await this.#resolve(event, context, options);
await this.#streamHandlerResponse(result, options.responseStream);
const reqCtx = await this.#resolve(event, context, options);
await this.#streamHandlerResponse(reqCtx, options.responseStream);
}

/**
* Streams a handler response to the Lambda response stream.
* Converts the response to a web response and pipes it through the stream.
*
* @param response - The handler response to stream
* @param reqCtx - The request context containing the response to stream
* @param responseStream - The Lambda response stream to write to
*/
async #streamHandlerResponse(
response: HandlerResponse,
reqCtx: RequestContext,
responseStream: ResponseStream
) {
const webResponse = handlerResultToWebResponse(response);
const { headers } = webHeadersToApiGatewayV1Headers(webResponse.headers);
const { headers } = webHeadersToApiGatewayHeaders(
reqCtx.res.headers,
reqCtx.responseType
);
const resStream = HttpResponseStream.from(responseStream, {
statusCode: webResponse.status,
statusCode: reqCtx.res.status,
headers,
});

if (webResponse.body) {
if (reqCtx.res.body) {
const nodeStream = Readable.fromWeb(
webResponse.body as streamWeb.ReadableStream
reqCtx.res.body as streamWeb.ReadableStream
);
await pipeline(nodeStream, resStream);
} else {
Expand Down
Loading
Loading