Skip to content

Instantly share code, notes, and snippets.

@notbrain
Created August 23, 2020 19:11
Show Gist options
  • Save notbrain/7390d17401c4c5d10fedd76a06363df2 to your computer and use it in GitHub Desktop.
Save notbrain/7390d17401c4c5d10fedd76a06363df2 to your computer and use it in GitHub Desktop.
Lambda NodeJS 12.18 Default Runtime JavaScript
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
"use strict";
/**
* The runtime has a single beforeExit function which is stored in the global
* object with a symbol key.
* The symbol is not exported.
* The process.beforeExit listener is setup in index.js along with all other
* top-level process event listeners.
*/
// define a named symbol for the handler function
const LISTENER_SYMBOL = Symbol.for("aws.lambda.beforeExit");
const NO_OP_LISTENER = () => {};
// export a setter
module.exports = {
/**
* Call the listener function with no arguments.
*/
invoke: () => global[LISTENER_SYMBOL](),
/**
* Reset the listener to a no-op function.
*/
reset: () => (global[LISTENER_SYMBOL] = NO_OP_LISTENER),
/**
* Set the listener to the provided function.
*/
set: listener => (global[LISTENER_SYMBOL] = listener)
};
#!/bin/sh
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
if [ -z "$NODE_PATH" ];
then
nodejs_mods="/opt/nodejs/node_modules"
nodejs12_mods="/opt/nodejs/node12/node_modules"
runtime_mods="/var/runtime/node_modules"
task="/var/runtime:/var/task"
export NODE_PATH="$nodejs12_mods:$nodejs_mods:$runtime_mods:$task"
fi
if [ -n "$AWS_LAMBDA_FUNCTION_MEMORY_SIZE" ];
then
new_space=$(expr $AWS_LAMBDA_FUNCTION_MEMORY_SIZE / 10)
semi_space=$(expr $new_space / 2)
old_space=$(expr $AWS_LAMBDA_FUNCTION_MEMORY_SIZE - $new_space)
MEMORY_ARGS=(
"--max-semi-space-size=$semi_space"
"--max-old-space-size=$old_space"
)
fi
if [[ $AWS_DEFAULT_REGION == us-iso* ]]
then export NODE_EXTRA_CA_CERTS="/etc/pki/tls/certs/ca-bundle.crt"
fi
export AWS_EXECUTION_ENV=AWS_Lambda_nodejs12.x
exec env \
/var/lang/bin/node \
--expose-gc \
--max-http-header-size 81920 \
"${MEMORY_ARGS[@]}" \
/var/runtime/index.js
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
"use strict";
let BeforeExitListener = require("./BeforeExitListener.js");
let Errors = require("./Errors");
function _homogeneousError(err) {
if (err instanceof Error) {
return err;
} else {
return new Error(err);
}
}
/**
* Build the callback function and the part of the context which exposes
* the succeed/fail/done callbacks.
* @param client {Client}
* The RAPID client used to post results/errors.
* @param id {string}
* The invokeId for the current invocation.
* @param scheduleNext {function}
* A function which takes no params and immediately schedules the next
* iteration of the invoke loop.
*/
function _rawCallbackContext(client, id, scheduleNext) {
const postError = (err, callback) => {
console.error("Invoke Error", Errors.toFormatted(_homogeneousError(err)));
client.postInvocationError(err, id, callback);
};
const complete = (result, callback) => {
client.postInvocationResponse(result, id, callback);
};
let waitForEmptyEventLoop = true;
let callback = function(err, result) {
BeforeExitListener.reset();
if (err !== undefined && err !== null) {
postError(err, scheduleNext);
} else {
complete(result, () => {
if (!waitForEmptyEventLoop) {
scheduleNext();
} else {
BeforeExitListener.set(scheduleNext);
}
});
}
};
let done = (err, result) => {
BeforeExitListener.reset();
if (err !== undefined && err !== null) {
postError(err, scheduleNext);
} else {
complete(result, scheduleNext);
}
};
let succeed = result => {
done(null, result);
};
let fail = err => {
if (err === undefined || err === null) {
done("handled");
} else {
done(err, null);
}
};
let callbackContext = {
get callbackWaitsForEmptyEventLoop() {
return waitForEmptyEventLoop;
},
set callbackWaitsForEmptyEventLoop(value) {
waitForEmptyEventLoop = value;
},
succeed: succeed,
fail: fail,
done: done
};
return [callback, callbackContext];
}
/**
* Wraps the callback and context so that only the first call to any callback
* succeeds.
* @param callback {function}
* the node-style callback function that was previously generated but not
* yet wrapped.
* @param callbackContext {object}
* The previously generated callbackContext object that contains
* getter/setters for the contextWaitsForEmptyeventLoop flag and the
* succeed/fail/done functions.
* @return [callback, context]
*/
function _wrappedCallbackContext(callback, callbackContext) {
let finished = false;
let onlyAllowFirstCall = function(toWrap) {
return function() {
if (!finished) {
toWrap.apply(null, arguments);
finished = true;
}
};
};
callbackContext.succeed = onlyAllowFirstCall(callbackContext.succeed);
callbackContext.fail = onlyAllowFirstCall(callbackContext.fail);
callbackContext.done = onlyAllowFirstCall(callbackContext.done);
return [onlyAllowFirstCall(callback), callbackContext];
}
/**
* Construct the base-context object which includes the required flags and
* callback methods for the Node programming model.
* @param client {Client}
* The RAPID client used to post results/errors.
* @param id {string}
* The invokeId for the current invocation.
* @param scheduleNext {function}
* A function which takes no params and immediately schedules the next
* iteration of the invoke loop.
* @return [callback, context]
* The same function and context object, but wrapped such that only the
* first call to any function will be successful. All subsequent calls are
* a no-op.
*/
module.exports.build = function(client, id, scheduleNext) {
let rawCallbackContext = _rawCallbackContext(client, id, scheduleNext);
return _wrappedCallbackContext(...rawCallbackContext);
};
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Defines custom error types throwable by the runtime.
*/
"use strict";
const util = require("util");
function _isError(obj) {
return (
obj &&
obj.name &&
obj.message &&
obj.stack &&
typeof obj.name === "string" &&
typeof obj.message === "string" &&
typeof obj.stack === "string"
);
}
/**
* Attempt to convert an object into a response object.
* This method accounts for failures when serializing the error object.
*/
function toRapidResponse(error) {
try {
if (util.types.isNativeError(error) || _isError(error)) {
return {
errorType: error.name,
errorMessage: error.message,
trace: error.stack.split("\n")
};
} else {
return {
errorType: typeof error,
errorMessage: error.toString(),
trace: []
};
}
} catch (_err) {
return {
errorType: "handled",
errorMessage:
"callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack"
};
}
}
module.exports.toRapidResponse = toRapidResponse;
/**
* Format an error with the expected properties.
* For compatability, the error string always starts with a tab.
*/
module.exports.toFormatted = error => {
try {
return (
"\t" + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v))
);
} catch (err) {
return "\t" + JSON.stringify(toRapidResponse(error));
}
};
/**
* Error name, message, code, and stack are all members of the superclass, which
* means they aren't enumerable and don't normally show up in JSON.stringify.
* This method ensures those interesting properties are available along with any
* user-provided enumerable properties.
*/
function _withEnumerableProperties(error) {
if (error instanceof Error) {
let ret = Object.assign(
{
errorType: error.name,
errorMessage: error.message,
code: error.code
},
error
);
if (typeof error.stack == "string") {
ret.stack = error.stack.split("\n");
}
return ret;
} else {
return error;
}
}
const errorClasses = [
class ImportModuleError extends Error {},
class HandlerNotFound extends Error {},
class MalformedHandlerName extends Error {},
class UserCodeSyntaxError extends Error {},
class UnhandledPromiseRejection extends Error {
constructor(reason, promise) {
super(reason);
this.reason = reason;
this.promise = promise;
}
}
];
errorClasses.forEach(e => {
module.exports[e.name] = e;
e.prototype.name = `Runtime.${e.name}`;
});
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module is the bootstrap entrypoint. It establishes the top-level event
* listeners and loads the user's code.
*/
"use strict";
const RAPIDClient = require("./RAPIDClient.js");
const Runtime = require("./Runtime.js");
const UserFunction = require("./UserFunction.js");
const Errors = require("./Errors.js");
const BeforeExitListener = require("./BeforeExitListener.js");
require("./LogPatch").patchConsole();
const client = new RAPIDClient(process.env.AWS_LAMBDA_RUNTIME_API);
let errorCallbacks = {
uncaughtException: error => {
client.postInitError(error, () => process.exit(129));
},
unhandledRejection: error => {
client.postInitError(error, () => process.exit(128));
}
};
process.on("uncaughtException", error => {
console.error("Uncaught Exception", Errors.toFormatted(error));
errorCallbacks.uncaughtException(error);
});
process.on("unhandledRejection", (reason, promise) => {
let error = new Errors.UnhandledPromiseRejection(reason, promise);
console.error("Unhandled Promise Rejection", Errors.toFormatted(error));
errorCallbacks.unhandledRejection(error);
});
BeforeExitListener.reset();
process.on("beforeExit", BeforeExitListener.invoke);
const handler = UserFunction.load(
process.env.LAMBDA_TASK_ROOT,
process.env._HANDLER
);
new Runtime(client, handler, errorCallbacks).scheduleIteration();
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module defines the InvokeContext and supporting functions. The
* InvokeContext is responsible for pulling information from the invoke headers
* and for wrapping the Rapid Client object's error and response functions.
*/
"use strict";
const assert = require("assert").strict;
let { setCurrentRequestId } = require("./LogPatch");
const INVOKE_HEADER = {
ClientContext: "lambda-runtime-client-context",
CognitoIdentity: "lambda-runtime-cognito-identity",
ARN: "lambda-runtime-invoked-function-arn",
AWSRequestId: "lambda-runtime-aws-request-id",
DeadlineMs: "lambda-runtime-deadline-ms",
XRayTrace: "lambda-runtime-trace-id"
};
module.exports = class InvokeContext {
constructor(headers) {
this.headers = _enforceLowercaseKeys(headers);
}
/**
* The invokeId for this request.
*/
get invokeId() {
let id = this.headers[INVOKE_HEADER.AWSRequestId];
assert.ok(id, "invocation id is missing or invalid");
return id;
}
/**
* Push relevant invoke data into the logging context.
*/
updateLoggingContext() {
setCurrentRequestId(this.invokeId);
}
/**
* Attach all of the relavant environmental and invocation data to the
* provided object.
* This method can throw if the headers are malformed and cannot be parsed.
* @param callbackContext {Object}
* The callbackContext object returned by a call to buildCallbackContext().
* @return {Object}
* The user context object with all required data populated from the headers
* and environment variables.
*/
attachEnvironmentData(callbackContext) {
this._forwardXRay();
return Object.assign(
callbackContext,
this._environmentalData(),
this._headerData()
);
}
/**
* All parts of the user-facing context object which are provided through
* environment variables.
*/
_environmentalData() {
return {
functionVersion: process.env["AWS_LAMBDA_FUNCTION_VERSION"],
functionName: process.env["AWS_LAMBDA_FUNCTION_NAME"],
memoryLimitInMB: process.env["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"],
logGroupName: process.env["AWS_LAMBDA_LOG_GROUP_NAME"],
logStreamName: process.env["AWS_LAMBDA_LOG_STREAM_NAME"]
};
}
/**
* All parts of the user-facing context object which are provided through
* request headers.
*/
_headerData() {
const deadline = this.headers[INVOKE_HEADER.DeadlineMs];
return {
clientContext: _parseJson(
this.headers[INVOKE_HEADER.ClientContext],
"ClientContext"
),
identity: _parseJson(
this.headers[INVOKE_HEADER.CognitoIdentity],
"CognitoIdentity"
),
invokedFunctionArn: this.headers[INVOKE_HEADER.ARN],
awsRequestId: this.headers[INVOKE_HEADER.AWSRequestId],
getRemainingTimeInMillis: function() {
return deadline - Date.now();
}
};
}
/**
* Forward the XRay header into the environment variable.
*/
_forwardXRay() {
if (this.headers[INVOKE_HEADER.XRayTrace]) {
process.env["_X_AMZN_TRACE_ID"] = this.headers[INVOKE_HEADER.XRayTrace];
} else {
delete process.env["_X_AMZN_TRACE_ID"];
}
}
};
/**
* Parse a JSON string and throw a readable error if something fails.
* @param jsonString {string} - the string to attempt to parse
* @param name {name} - the name to use when describing the string in an error
* @return object - the parsed object
* @throws if jsonString cannot be parsed
*/
function _parseJson(jsonString, name) {
if (jsonString !== undefined) {
try {
return JSON.parse(jsonString);
} catch (err) {
throw new Error(`Cannot parse ${name} as json: ${err.toString()}`);
}
} else {
return undefined;
}
}
/**
* Construct a copy of an object such that all of its keys are lowercase.
*/
function _enforceLowercaseKeys(original) {
return Object.keys(original).reduce((enforced, originalKey) => {
enforced[originalKey.toLowerCase()] = original[originalKey];
return enforced;
}, {});
}
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
"use strict";
const util = require("util");
const fs = require("fs");
const levels = Object.freeze({
INFO: { name: "INFO" },
DEBUG: { name: "DEBUG" },
WARN: { name: "WARN" },
ERROR: { name: "ERROR" },
TRACE: { name: "TRACE" },
FATAL: { name: "FATAL" }
});
/* Use a unique symbol to provide global access without risk of name clashes. */
const REQUEST_ID_SYMBOL = Symbol.for("aws.lambda.runtime.requestId");
let _currentRequestId = {
get: () => global[REQUEST_ID_SYMBOL],
set: id => (global[REQUEST_ID_SYMBOL] = id)
};
/**
* Write logs to stdout.
*/
let _logToStdout = (level, message) => {
let time = new Date().toISOString();
let requestId = _currentRequestId.get();
let line = `${time}\t${requestId}\t${level.name}\t${message}`;
line = line.replace(/\n/g, "\r");
process.stdout.write(line + "\n");
};
/**
* Write logs to filedescriptor.
* Implements the logging contract between runtimes and the platform.
* Each entry is framed as:
* +----------------------+------------------------+-----------------------+
* | Frame Type - 4 bytes | Length (len) - 4 bytes | Message - 'len' bytes |
* +----------------------+------------------------+-----------------------+
* The frist 4 bytes are the frame type. For logs this is always 0xa55a0001.
* The second 4 bytes are the length of the message.
* The remaining bytes ar ethe message itself. Byte order is big-endian.
*/
let _logToFd = function(logTarget) {
let typeAndLength = Buffer.alloc(8);
typeAndLength.writeUInt32BE(0xa55a0001, 0);
typeAndLength.writeUInt32BE(0x00000000, 4);
return (level, message) => {
let time = new Date().toISOString();
let requestId = _currentRequestId.get();
let enrichedMessage = `${time}\t${requestId}\t${level.name}\t${message}\n`;
let messageBytes = Buffer.from(enrichedMessage, "utf8");
typeAndLength.writeInt32BE(messageBytes.length, 4);
fs.writeSync(logTarget, typeAndLength);
fs.writeSync(logTarget, messageBytes);
};
};
/**
* Replace console functions with a log function.
* @param {Function(level, String)} log
*/
function _patchConsoleWith(log) {
console.log = (msg, ...params) => {
log(levels.INFO, util.format(msg, ...params));
};
console.debug = (msg, ...params) => {
log(levels.DEBUG, util.format(msg, ...params));
};
console.info = (msg, ...params) => {
log(levels.INFO, util.format(msg, ...params));
};
console.warn = (msg, ...params) => {
log(levels.WARN, util.format(msg, ...params));
};
console.error = (msg, ...params) => {
log(levels.ERROR, util.format(msg, ...params));
};
console.trace = (msg, ...params) => {
log(levels.TRACE, util.format(msg, ...params));
};
console.fatal = (msg, ...params) => {
log(levels.FATAL, util.format(msg, ...params));
};
}
let _patchConsole = () => {
if (
process.env["_LAMBDA_TELEMETRY_LOG_FD"] != null &&
process.env["_LAMBDA_TELEMETRY_LOG_FD"] != undefined
) {
let logFd = parseInt(process.env["_LAMBDA_TELEMETRY_LOG_FD"]);
_patchConsoleWith(_logToFd(logFd));
delete process.env["_LAMBDA_TELEMETRY_LOG_FD"];
} else {
_patchConsoleWith(_logToStdout);
}
};
module.exports = {
setCurrentRequestId: _currentRequestId.set,
patchConsole: _patchConsole
};
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module defines the RAPID client which is responsible for all HTTP
* interactions with the RAPID layer.
*/
"use strict";
const Errors = require("./Errors");
const XRayError = require("./XRayError");
const ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type";
/**
* Objects of this class are responsible for all interactions with the RAPID
* API.
*/
module.exports = class RAPIDClient {
constructor(hostnamePort, httpClient, nativeClient) {
this.http = httpClient || require("http");
this.nativeClient =
nativeClient || require("/var/runtime/build/Release/rapid-client.node");
let [hostname, port] = hostnamePort.split(":");
this.hostname = hostname;
this.port = parseInt(port, 10);
this.agent = new this.http.Agent({
keepAlive: true,
maxSockets: 1
});
}
/**
* Complete and invocation with the provided response.
* @param {Object} response
* An arbitrary object to convert to JSON and send back as as response.
* @param {String} id
* The invocation ID.
* @param (function()} callback
* The callback to run after the POST response ends
*/
postInvocationResponse(response, id, callback) {
let bodyString = _trySerializeResponse(response);
this.nativeClient.done(id, bodyString);
callback();
}
/**
* Post an initialization error to the RAPID API.
* @param {Error} error
* @param (function()} callback
* The callback to run after the POST response ends
*/
postInitError(error, callback) {
let response = Errors.toRapidResponse(error);
this._post(
`/2018-06-01/runtime/init/error`,
response,
{ [ERROR_TYPE_HEADER]: response.errorType },
callback
);
}
/**
* Post an invocation error to the RAPID API
* @param {Error} error
* @param {String} id
* The invocation ID for the in-progress invocation.
* @param (function()} callback
* The callback to run after the POST response ends
*/
postInvocationError(error, id, callback) {
let response = Errors.toRapidResponse(error);
let bodyString = _trySerializeResponse(response);
let xrayString = XRayError.formatted(error);
this.nativeClient.error(id, bodyString, xrayString);
callback();
}
/**
* Get the next invocation.
* @return {PromiseLike.<Object>}
* A promise which resolves to an invocation object that contains the body
* as json and the header array. e.g. {bodyJson, headers}
*/
async nextInvocation() {
return this.nativeClient.next();
}
/**
* HTTP Post to a path.
* @param {String} path
* @param {Object} body
* The body is serialized into JSON before posting.
* @param {Object} headers
* The http headers
* @param (function()} callback
* The callback to run after the POST response ends
*/
_post(path, body, headers, callback) {
let bodyString = _trySerializeResponse(body);
let options = {
hostname: this.hostname,
port: this.port,
path: path,
method: "POST",
headers: Object.assign(
{
"Content-Type": "application/json",
"Content-Length": Buffer.from(bodyString).length
},
headers || {}
),
agent: this.agent
};
let request = this.http.request(options, response => {
response.on("end", () => {
callback();
});
response.on("error", e => {
throw e;
});
response.on("data", () => {});
});
request.on("error", e => {
throw e;
});
request.end(bodyString, "utf-8");
}
};
/**
* Attempt to serialize an object as json. Capture the failure if it occurs and
* throw one that's known to be serializable.
*/
function _trySerializeResponse(body) {
try {
return JSON.stringify(body === undefined ? null : body);
} catch (err) {
throw new Error("Unable to stringify response body");
}
}
NAME=Node.js
VERSION=12.18.0
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module defines the top-level Runtime class which controls the
* bootstrap's execution flow.
*/
"use strict";
const InvokeContext = require("./InvokeContext.js");
const CallbackContext = require("./CallbackContext.js");
const BeforeExitListener = require("./BeforeExitListener.js");
module.exports = class Runtime {
constructor(client, handler, errorCallbacks) {
this.client = client;
this.handler = handler;
this.errorCallbacks = errorCallbacks;
}
/**
* Schedule the next loop iteration to start at the beginning of the next time
* around the event loop.
*/
scheduleIteration() {
let that = this;
setImmediate(() => {
that.handleOnce().then(
// Success is a no-op at this level. There are 2 cases:
// 1 - The user used one of the callback functions which already
// schedules the next iteration.
// 2 - The next iteration is not scheduled because the
// waitForEmptyEventLoop was set. In this case the beforeExit
// handler will automatically start the next iteration.
() => {},
// Errors should not reach this level in typical execution. If they do
// it's a sign of an issue in the Client or a bug in the runtime. So
// dump it to the console and attempt to report it as a Runtime error.
err => {
console.log(`Unexpected Top Level Error: ${err.toString()}`);
this.errorCallbacks.uncaughtException(err);
}
);
});
}
/**
* Wait for the next invocation, process it, and schedule the next iteration.
*/
async handleOnce() {
let { bodyJson, headers } = await this.client.nextInvocation();
let invokeContext = new InvokeContext(headers);
invokeContext.updateLoggingContext();
let [callback, callbackContext] = CallbackContext.build(
this.client,
invokeContext.invokeId,
this.scheduleIteration.bind(this)
);
try {
this._setErrorCallbacks(invokeContext.invokeId);
this._setDefaultExitListener(invokeContext.invokeId);
let result = this.handler(
JSON.parse(bodyJson),
invokeContext.attachEnvironmentData(callbackContext),
callback
);
if (_isPromise(result)) {
result
.then(callbackContext.succeed, callbackContext.fail)
.catch(callbackContext.fail);
}
} catch (err) {
callback(err);
}
}
/**
* Replace the error handler callbacks.
* @param {String} invokeId
*/
_setErrorCallbacks(invokeId) {
this.errorCallbacks.uncaughtException = error => {
this.client.postInvocationError(error, invokeId, () => {
process.exit(129);
});
};
this.errorCallbacks.unhandledRejection = error => {
this.client.postInvocationError(error, invokeId, () => {
process.exit(128);
});
};
}
/**
* Setup the 'beforeExit' listener that is used if the callback is never
* called and the handler is not async.
* CallbackContext replaces the listener if a callback is invoked.
*/
_setDefaultExitListener(invokeId) {
BeforeExitListener.set(() => {
this.client.postInvocationResponse(null, invokeId, () =>
this.scheduleIteration()
);
});
}
};
function _isPromise(obj) {
return obj && obj.then && typeof obj.then === "function";
}
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This module defines the functions for loading the user's code as specified
* in a handler string.
*/
"use strict";
const {
HandlerNotFound,
MalformedHandlerName,
ImportModuleError,
UserCodeSyntaxError
} = require("./Errors.js");
const path = require("path");
const fs = require("fs");
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
const RELATIVE_PATH_SUBSTRING = "..";
/**
* Break the full handler string into two pieces, the module root and the actual
* handler string.
* Given './somepath/something/module.nestedobj.handler' this returns
* ['./somepath/something', 'module.nestedobj.handler']
*/
function _moduleRootAndHandler(fullHandlerString) {
let handlerString = path.basename(fullHandlerString);
let moduleRoot = fullHandlerString.substring(
0,
fullHandlerString.indexOf(handlerString)
);
return [moduleRoot, handlerString];
}
/**
* Split the handler string into two pieces: the module name and the path to
* the handler function.
*/
function _splitHandlerString(handler) {
let match = handler.match(FUNCTION_EXPR);
if (!match || match.length != 3) {
throw new MalformedHandlerName("Bad handler");
}
return [match[1], match[2]]; // [module, function-path]
}
/**
* Resolve the user's handler function from the module.
*/
function _resolveHandler(object, nestedProperty) {
return nestedProperty.split(".").reduce((nested, key) => {
return nested && nested[key];
}, object);
}
/**
* Verify that the provided path can be loaded as a file per:
* https://nodejs.org/dist/latest-v10.x/docs/api/modules.html#modules_all_together
* @param string - the fully resolved file path to the module
* @return bool
*/
function _canLoadAsFile(modulePath) {
return fs.existsSync(modulePath) || fs.existsSync(modulePath + ".js");
}
/**
* Attempt to load the user's module.
* Attempts to directly resolve the module relative to the application root,
* then falls back to the more general require().
*/
function _tryRequire(appRoot, moduleRoot, module) {
let lambdaStylePath = path.resolve(appRoot, moduleRoot, module);
if (_canLoadAsFile(lambdaStylePath)) {
return require(lambdaStylePath);
} else {
// Why not just require(module)?
// Because require() is relative to __dirname, not process.cwd(). And the
// runtime implementation is not located in /var/task
let nodeStylePath = require.resolve(module, {
paths: [appRoot, moduleRoot]
});
return require(nodeStylePath);
}
}
/**
* Load the user's application or throw a descriptive error.
* @throws Runtime errors in two cases
* 1 - UserCodeSyntaxError if there's a syntax error while loading the module
* 2 - ImportModuleError if the module cannot be found
*/
function _loadUserApp(appRoot, moduleRoot, module) {
try {
return _tryRequire(appRoot, moduleRoot, module);
} catch (e) {
if (e instanceof SyntaxError) {
throw new UserCodeSyntaxError(e);
} else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") {
throw new ImportModuleError(e);
} else {
throw e;
}
}
}
function _throwIfInvalidHandler(fullHandlerString) {
if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) {
throw new MalformedHandlerName(
`'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.`
);
}
}
/**
* Load the user's function with the approot and the handler string.
* @param appRoot {string}
* The path to the application root.
* @param handlerString {string}
* The user-provided handler function in the form 'module.function'.
* @return userFuction {function}
* The user's handler function. This function will be passed the event body,
* the context object, and the callback function.
* @throws In five cases:-
* 1 - if the handler string is incorrectly formatted an error is thrown
* 2 - if the module referenced by the handler cannot be loaded
* 3 - if the function in the handler does not exist in the module
* 4 - if a property with the same name, but isn't a function, exists on the
* module
* 5 - the handler includes illegal character sequences (like relative paths
* for traversing up the filesystem '..')
* Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors.
*/
module.exports.load = function(appRoot, fullHandlerString) {
_throwIfInvalidHandler(fullHandlerString);
let [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(fullHandlerString);
let [module, handlerPath] = _splitHandlerString(moduleAndHandler);
let userApp = _loadUserApp(appRoot, moduleRoot, module);
let handlerFunc = _resolveHandler(userApp, handlerPath);
if (!handlerFunc) {
throw new HandlerNotFound(
`${fullHandlerString} is undefined or not exported`
);
}
if (typeof handlerFunc !== "function") {
throw new HandlerNotFound(`${fullHandlerString} is not a function`);
}
return handlerFunc;
};
/** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
"use strict";
module.exports.formatted = err => {
try {
return JSON.stringify(new XRayFormattedCause(err));
} catch (err) {
return "";
}
};
/**
* prepare an exception blob for sending to AWS X-Ray
* adapted from https://code.amazon.com/packages/AWSTracingSDKNode/blobs/c917508ca4fce6a795f95dc30c91b70c6bc6c617/--/core/lib/segments/attributes/captured_exception.js
* transform an Error, or Error-like, into an exception parseable by X-Ray's service.
* {
* "name": "CustomException",
* "message": "Something bad happend!",
* "stack": [
* "exports.handler (/var/task/node_modules/event_invoke.js:3:502)
* ]
* }
* =>
* {
* "working_directory": "/var/task",
* "exceptions": [
* {
* "type": "CustomException",
* "message": "Something bad happend!",
* "stack": [
* {
* "path": "/var/task/event_invoke.js",
* "line": 502,
* "label": "exports.throw_custom_exception"
* }
* ]
* }
* ],
* "paths": [
* "/var/task/event_invoke.js"
* ]
* }
*/
class XRayFormattedCause {
constructor(err) {
this.working_directory = process.cwd(); // eslint-disable-line
let stack = [];
if (err.stack) {
let stackLines = err.stack.split("\n");
stackLines.shift();
stackLines.forEach(stackLine => {
let line = stackLine.trim().replace(/\(|\)/g, "");
line = line.substring(line.indexOf(" ") + 1);
let label =
line.lastIndexOf(" ") >= 0
? line.slice(0, line.lastIndexOf(" "))
: null;
let path =
label == undefined || label == null || label.length === 0
? line
: line.slice(line.lastIndexOf(" ") + 1);
path = path.split(":");
let entry = {
path: path[0],
line: parseInt(path[1]),
label: label || "anonymous"
};
stack.push(entry);
});
}
this.exceptions = [
{
type: err.name,
message: err.message,
stack: stack
}
];
let paths = new Set();
stack.forEach(entry => {
paths.add(entry.path);
});
this.paths = Array.from(paths);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment