Skip to content

Vendor the load function from the Lambda runtime #238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 20, 2021
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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore code that was originally part of the Lambda runtime
runtime
1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Component,Origin,License,Copyright
async-listener,github.com/othiym23/async-listener,BSD-2-Clause,"Copyright (c) 2013-2017, Forrest L Norvell All rights reserved."
async,github.com/caolan/async,MIT,Copyright (c) 2010-2014 Caolan McMahon
atomic-batcher,github.com/mafintosh/atomic-batcher,MIT,Copyright (c) 2016 Mathias Buus
aws-lambda-nodejs-runtime-interface-client,github.com/aws/aws-lambda-nodejs-runtime-interface-client,Apache 2.0,Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
aws-sdk,github.com/aws/aws-sdk-js,Apache-2.0,"Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved."
aws-xray-sdk-core,github.com/aws/aws-xray-sdk-node/tree/master/packages/core,Apache-2.0,"Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved."
base64-js,github.com/beatgammit/base64-js,MIT,Copyright (c) 2014 Jameson Little
Expand Down
3 changes: 3 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ Datadog datadog-lambda-js
Copyright 2019 Datadog, Inc.

This product includes software developed at Datadog (https://www.datadoghq.com/).

The Initial Developer of the files in the runtime directory is Amazon.com, Inc.
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 changes: 1 addition & 3 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { datadog, datadogHandlerEnvVar, lambdaTaskRootEnvVar, traceExtractorEnvVar, getEnvValue } from "./index";
import { TraceExtractor } from "./trace";
import { logDebug, logError } from "./utils";
// We reuse the function loading logic already inside the lambda runtime.
// tslint:disable-next-line:no-var-requires
const { load } = require("/var/runtime/UserFunction") as any;
import { load } from "./runtime";

if (process.env.DD_TRACE_DISABLED_PLUGINS === undefined) {
process.env.DD_TRACE_DISABLED_PLUGINS = "fs";
Expand Down
137 changes: 137 additions & 0 deletions src/runtime/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Defines custom error types throwable by the runtime.
*/

"use strict";

import util from "util";

export function isError(obj: any): obj is Error {
return (
obj &&
obj.name &&
obj.message &&
obj.stack &&
typeof obj.name === "string" &&
typeof obj.message === "string" &&
typeof obj.stack === "string"
);
}

interface RuntimeErrorResponse {
errorType: string;
errorMessage: string;
trace: string[];
}

/**
* Attempt to convert an object into a response object.
* This method accounts for failures when serializing the error object.
*/
export function toRuntimeResponse(error: unknown): RuntimeErrorResponse {
try {
if (util.types.isNativeError(error) || isError(error)) {
if (!error.stack) {
throw new Error("Error stack is missing.");
}
return {
errorType: error.name,
errorMessage: error.message,
trace: error.stack.split("\n") || [],
};
} else {
return {
errorType: typeof error,
errorMessage: (error as any).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",
trace: [],
};
}
}

/**
* Format an error with the expected properties.
* For compatability, the error string always starts with a tab.
*/
export const toFormatted = (error: unknown): string => {
try {
return (
"\t" + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v))
);
} catch (err) {
return "\t" + JSON.stringify(toRuntimeResponse(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: any) {
if (error instanceof Error) {
const extendedError: ExtendedError = <ExtendedError>(<any>error);
const ret: any = Object.assign(
{
errorType: extendedError.name,
errorMessage: extendedError.message,
code: extendedError.code,
},
extendedError
);
if (typeof extendedError.stack == "string") {
ret.stack = extendedError.stack.split("\n");
}
return ret;
} else {
return error;
}
}

export class ExtendedError extends Error {
code?: number;
custom?: string;
reason?: string;
promise?: Promise<any>;

constructor(reason?: string) {
super(reason); // 'Error' breaks prototype chain here
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}
}

export class ImportModuleError extends ExtendedError {}
export class HandlerNotFound extends ExtendedError {}
export class MalformedHandlerName extends ExtendedError {}
export class UserCodeSyntaxError extends ExtendedError {}
export class UnhandledPromiseRejection extends ExtendedError {
constructor(reason?: string, promise?: Promise<any>) {
super(reason);
this.reason = reason;
this.promise = promise;
}
}

const errorClasses = [
ImportModuleError,
HandlerNotFound,
MalformedHandlerName,
UserCodeSyntaxError,
UnhandledPromiseRejection,
];

errorClasses.forEach((e) => {
e.prototype.name = `Runtime.${e.name}`;
});
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { load } from "./user-function";
170 changes: 170 additions & 0 deletions src/runtime/user-function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Modifications copyright 2021 Datadog, Inc.
*
* The original file was part of aws-lambda-nodejs-runtime-interface-client
* https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/utils/UserFunction.ts
*
* This module defines the functions for loading the user's code as specified
* in a handler string.
*/

"use strict";

import path from "path";
import fs from "fs";
import {
HandlerNotFound,
MalformedHandlerName,
ImportModuleError,
UserCodeSyntaxError,
ExtendedError,
} from "./errors";

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: string): [string, string] {
const handlerString = path.basename(fullHandlerString);
const 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: string): [string, string] {
const 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: any, nestedProperty: string): any {
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: string): boolean {
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: string, moduleRoot: string, module: string): any {
const 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()
const 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: string,
moduleRoot: string,
module: string
): any {
try {
return _tryRequire(appRoot, moduleRoot, module);
} catch (e) {
if (e instanceof SyntaxError) {
throw new UserCodeSyntaxError(<any>e);
// @ts-ignore
} else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") {
// @ts-ignore
throw new ImportModuleError(e);
} else {
throw e;
}
}
}

function _throwIfInvalidHandler(fullHandlerString: string): void {
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.
*/
export const load = function (
appRoot: string,
fullHandlerString: string
) {
_throwIfInvalidHandler(fullHandlerString);

const [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(
fullHandlerString
);
const [module, handlerPath] = _splitHandlerString(moduleAndHandler);

const userApp = _loadUserApp(appRoot, moduleRoot, module);
const 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;
};
5 changes: 5 additions & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"extends": "tslint:recommended",
"linterOptions": {
"exclude": [
"**/runtime/**"
]
},
"rules": {
"interface-name": false,
"variable-name": {
Expand Down