Skip to content
Merged
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
54 changes: 32 additions & 22 deletions packages/event-handler/src/rest/BaseRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import type {
HttpMethod,
Middleware,
Path,
RequestContext,
RouteHandler,
RouteOptions,
RouterOptions,
} from '../types/rest.js';
import { HttpErrorCodes, HttpVerbs } from './constants.js';
import {
handlerResultToProxyResult,
handlerResultToResponse,
proxyEventToWebRequest,
responseToProxyResult,
} from './converters.js';
Expand Down Expand Up @@ -209,6 +211,15 @@ abstract class BaseRouter {

const request = proxyEventToWebRequest(event);

const handlerOptions: RequestContext = {
event,
context,
request,
// this response should be overwritten by the handler, if it isn't
// it means somthing went wrong with the middleware chain
res: new Response('', { status: 500 }),
};

try {
const path = new URL(request.url).pathname as Path;

Expand All @@ -223,34 +234,36 @@ abstract class BaseRouter {
? route.handler.bind(options.scope)
: route.handler;

const handlerMiddleware: Middleware = async (params, options, next) => {
const handlerResult = await handler(params, options);
options.res = handlerResultToResponse(
handlerResult,
options.res.headers
);

await next();
};

const middleware = composeMiddleware([
...this.middleware,
...route.middleware,
handlerMiddleware,
]);

const result = await middleware(
const middlewareResult = await middleware(
route.params,
{
event,
context,
request,
},
() => handler(route.params, { event, context, request })
handlerOptions,
() => Promise.resolve()
);

// In practice this we never happen because the final 'middleware' is
// the handler function that allways returns HandlerResponse. However, the
// type signature of of NextFunction includes undefined so we need this for
// the TS compiler
if (result === undefined) throw new InternalServerError();
// middleware result takes precedence to allow short-circuiting
const result = middlewareResult ?? handlerOptions.res;

return await handlerResultToProxyResult(result);
return handlerResultToProxyResult(result);
} catch (error) {
this.logger.debug(`There was an error processing the request: ${error}`);
const result = await this.handleError(error as Error, {
request,
event,
context,
...handlerOptions,
scope: options?.scope,
});
return await responseToProxyResult(result);
Expand Down Expand Up @@ -281,13 +294,10 @@ abstract class BaseRouter {
const handler = this.errorHandlerRegistry.resolve(error);
if (handler !== null) {
try {
const body = await handler.apply(options.scope ?? this, [
const { scope, ...handlerOptions } = options;
const body = await handler.apply(scope ?? this, [
error,
{
request: options.request,
event: options.event,
context: options.context,
},
handlerOptions,
]);
return new Response(JSON.stringify(body), {
status: body.statusCode,
Expand Down
53 changes: 50 additions & 3 deletions packages/event-handler/src/rest/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,60 @@ export const responseToProxyResult = async (
}
}

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

if (Object.keys(multiValueHeaders).length > 0) {
result.multiValueHeaders = multiValueHeaders;
}

return result;
};

/**
* Converts a handler response to a Web API Response object.
* Handles APIGatewayProxyResult, Response objects, and plain objects.
*
* @param response - The handler response (APIGatewayProxyResult, Response, or plain object)
* @param headers - Optional headers to be included in the response
* @returns A Web API Response object
*/
export const handlerResultToResponse = (
response: HandlerResponse,
resHeaders?: Headers
): Response => {
if (response instanceof Response) {
return response;
}

const headers = new Headers(resHeaders);
headers.set('Content-Type', 'application/json');

if (isAPIGatewayProxyResult(response)) {
for (const [key, value] of Object.entries(response.headers ?? {})) {
if (value != null) {
headers.set(key, String(value));
}
}

for (const [key, values] of Object.entries(
response.multiValueHeaders ?? {}
)) {
for (const value of values ?? []) {
headers.append(key, String(value));
}
}

return new Response(response.body, {
status: response.statusCode,
headers,
});
}
return Response.json(response, { headers });
};

/**
Expand All @@ -117,7 +164,7 @@ export const handlerResultToProxyResult = async (
return {
statusCode: 200,
body: JSON.stringify(response),
headers: { 'Content-Type': 'application/json' },
headers: { 'content-type': 'application/json' },
isBase64Encoded: false,
};
};
4 changes: 2 additions & 2 deletions packages/event-handler/src/rest/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
HttpMethod,
Middleware,
Path,
RequestOptions,
RequestContext,
ValidationResult,
} from '../types/rest.js';
import {
Expand Down Expand Up @@ -146,7 +146,7 @@ export const isAPIGatewayProxyResult = (
export const composeMiddleware = (middleware: Middleware[]): Middleware => {
return async (
params: Record<string, string>,
options: RequestOptions,
options: RequestContext,
next: () => Promise<HandlerResponse | void>
): Promise<HandlerResponse | void> => {
let index = -1;
Expand Down
13 changes: 7 additions & 6 deletions packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ type ErrorResponse = {
message: string;
};

type RequestOptions = {
type RequestContext = {
request: Request;
event: APIGatewayProxyEvent;
context: Context;
res: Response;
};

type ErrorResolveOptions = RequestOptions & ResolveOptions;
type ErrorResolveOptions = RequestContext & ResolveOptions;

type ErrorHandler<T extends Error = Error> = (
error: T,
options: RequestOptions
options: RequestContext
) => Promise<ErrorResponse>;

interface ErrorConstructor<T extends Error = Error> {
Expand Down Expand Up @@ -58,7 +59,7 @@ type HandlerResponse = Response | JSONObject;
type RouteHandler<
TParams = Record<string, unknown>,
TReturn = HandlerResponse,
> = (args: TParams, options: RequestOptions) => Promise<TReturn>;
> = (args: TParams, options: RequestContext) => Promise<TReturn>;

type HttpMethod = keyof typeof HttpVerbs;

Expand All @@ -83,7 +84,7 @@ type NextFunction = () => Promise<HandlerResponse | void>;

type Middleware = (
params: Record<string, string>,
options: RequestOptions,
options: RequestContext,
next: NextFunction
) => Promise<void | HandlerResponse>;

Expand Down Expand Up @@ -123,7 +124,7 @@ export type {
HttpMethod,
Middleware,
Path,
RequestOptions,
RequestContext,
RouterOptions,
RouteHandler,
RouteOptions,
Expand Down
Loading