@middy/core
Version:
🛵 The stylish Node.js middleware engine for AWS Lambda (core package)
216 lines (193 loc) • 6.04 kB
JavaScript
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors.
// SPDX-License-Identifier: MIT
import { setTimeout } from "node:timers";
import { executionModeStandard } from "./executionModeStandard.js";
const defaultLambdaHandler = () => {};
const noop = () => {};
const defaultPluginConfig = {
timeoutEarlyInMillis: 5,
timeoutEarlyResponse: () => {
const err = new Error("[AbortError]: The operation was aborted.", {
cause: { package: "@middy/core" },
});
err.name = "TimeoutError";
throw err;
},
executionMode: executionModeStandard,
};
export const middy = (setupLambdaHandler, pluginConfig) => {
let lambdaHandler;
let plugin;
// Allow base handler to be set using .handler()
if (typeof setupLambdaHandler === "function") {
lambdaHandler = setupLambdaHandler;
plugin = { ...defaultPluginConfig, ...pluginConfig };
} else {
lambdaHandler = defaultLambdaHandler;
plugin = { ...defaultPluginConfig, ...setupLambdaHandler };
}
plugin.timeoutEarly = plugin.timeoutEarlyInMillis > 0;
// Pre-compute single-call plugin hooks as noop to avoid optional chaining
// Note: beforeMiddleware/afterMiddleware kept as optional chaining in runMiddlewares
// because V8 optimizes ?.() null-checks faster than noop calls in tight loops
plugin.requestStart ??= noop;
plugin.requestEnd ??= noop;
plugin.beforeHandler ??= noop;
plugin.afterHandler ??= noop;
plugin.beforePrefetch?.();
const beforeMiddlewares = [];
const afterMiddlewares = [];
const onErrorMiddlewares = [];
const middyRequest = (event = {}, context = {}) => {
return {
event,
context,
response: undefined,
error: undefined,
internal: plugin.internal ?? {},
};
};
const middy = plugin.executionMode(
{ middyRequest, runRequest },
beforeMiddlewares,
lambdaHandler,
afterMiddlewares,
onErrorMiddlewares,
plugin,
);
middy.use = (inputMiddleware) => {
const middlewares = Array.isArray(inputMiddleware)
? inputMiddleware
: [inputMiddleware];
for (const middleware of middlewares) {
const { before, after, onError } = middleware;
if (before || after || onError) {
if (before) middy.before(before);
if (after) middy.after(after);
if (onError) middy.onError(onError);
} else {
throw new Error(
'Middleware must be an object containing at least one key among "before", "after", "onError"',
{
cause: { package: "@middy/core" },
},
);
}
}
return middy;
};
// Inline Middlewares
middy.before = (beforeMiddleware) => {
beforeMiddlewares.push(beforeMiddleware);
return middy;
};
middy.after = (afterMiddleware) => {
afterMiddlewares.unshift(afterMiddleware);
return middy;
};
middy.onError = (onErrorMiddleware) => {
onErrorMiddlewares.unshift(onErrorMiddleware);
return middy;
};
return middy;
};
// shared AbortController, because it's slow
let handlerAbort = new AbortController();
let abortOpts = { signal: handlerAbort.signal };
const runRequest = async (
request,
beforeMiddlewares,
lambdaHandler,
afterMiddlewares,
onErrorMiddlewares,
plugin,
) => {
let timeoutID;
// context.getRemainingTimeInMillis checked for when AWS context missing (tests, containers)
const getRemainingTimeInMillis =
request.context.getRemainingTimeInMillis ||
request.context.lambdaContext?.getRemainingTimeInMillis;
const timeoutEarly = plugin.timeoutEarly && getRemainingTimeInMillis;
try {
await runMiddlewares(request, beforeMiddlewares, plugin);
// Check if before stack hasn't exit early
if (!("earlyResponse" in request)) {
plugin.beforeHandler();
// Can't manually abort and timeout with same AbortSignal
// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
if (handlerAbort.signal.aborted) {
handlerAbort = new AbortController();
abortOpts = { signal: handlerAbort.signal };
}
// clearTimeout pattern is 10x faster than using AbortController
// Note: signal.abort is slow ~6_000ns
// Required --test-force-exit to ignore unresolved timeoutPromise
const handlerResult = lambdaHandler(
request.event,
request.context,
abortOpts,
);
if (timeoutEarly) {
let timeoutResolve;
const timeoutPromise = new Promise((resolve, reject) => {
timeoutResolve = () => {
handlerAbort.abort();
try {
resolve(plugin.timeoutEarlyResponse());
} catch (err) {
reject(err);
}
};
});
timeoutID = setTimeout(
timeoutResolve,
getRemainingTimeInMillis() - plugin.timeoutEarlyInMillis,
);
request.response = await Promise.race([handlerResult, timeoutPromise]);
} else {
request.response = await handlerResult;
}
if (timeoutID) {
clearTimeout(timeoutID);
}
plugin.afterHandler();
await runMiddlewares(request, afterMiddlewares, plugin);
}
} catch (err) {
// timeout should be aborted when errors happen in handler
if (timeoutID) {
clearTimeout(timeoutID);
}
// Reset response changes made by after stack before error thrown
request.response = undefined;
request.error = err;
try {
await runMiddlewares(request, onErrorMiddlewares, plugin);
} catch (err) {
// Save error that wasn't handled
err.originalError = request.error;
request.error = err;
throw request.error;
}
// Catch if onError stack hasn't handled the error
if (typeof request.response === "undefined") throw request.error;
}
return request.response;
};
const runMiddlewares = async (request, middlewares, plugin) => {
for (const nextMiddleware of middlewares) {
plugin.beforeMiddleware?.(nextMiddleware.name);
const res = await nextMiddleware(request);
plugin.afterMiddleware?.(nextMiddleware.name);
// short circuit chaining and respond early
if (typeof res !== "undefined") {
request.earlyResponse = res;
}
// earlyResponse pattern added in 6.0.0 to handle undefined values
if ("earlyResponse" in request) {
request.response = request.earlyResponse;
return;
}
}
};
export default middy;