Skip to content

Commit a76337b

Browse files
committed
Vendor the load function
1 parent 4738b4a commit a76337b

File tree

5 files changed

+309
-3
lines changed

5 files changed

+309
-3
lines changed

NOTICE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ Datadog datadog-lambda-js
22
Copyright 2019 Datadog, Inc.
33

44
This product includes software developed at Datadog (https://www.datadoghq.com/).
5+
6+
The Initial Developer of the files in the runtime directory is Amazon.com, Inc.
7+
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.

src/handler.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { datadog, datadogHandlerEnvVar, lambdaTaskRootEnvVar, traceExtractorEnvVar, getEnvValue } from "./index";
22
import { TraceExtractor } from "./trace";
33
import { logDebug, logError } from "./utils";
4-
// We reuse the function loading logic already inside the lambda runtime.
5-
// tslint:disable-next-line:no-var-requires
6-
const { load } = require("/var/runtime/UserFunction") as any;
4+
import { load } from "./runtime";
75

86
if (process.env.DD_TRACE_DISABLED_PLUGINS === undefined) {
97
process.env.DD_TRACE_DISABLED_PLUGINS = "fs";

src/runtime/errors.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
/**
4+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Defines custom error types throwable by the runtime.
7+
*/
8+
9+
"use strict";
10+
11+
import util from "util";
12+
13+
export function isError(obj: any): obj is Error {
14+
return (
15+
obj &&
16+
obj.name &&
17+
obj.message &&
18+
obj.stack &&
19+
typeof obj.name === "string" &&
20+
typeof obj.message === "string" &&
21+
typeof obj.stack === "string"
22+
);
23+
}
24+
25+
interface RuntimeErrorResponse {
26+
errorType: string;
27+
errorMessage: string;
28+
trace: string[];
29+
}
30+
31+
/**
32+
* Attempt to convert an object into a response object.
33+
* This method accounts for failures when serializing the error object.
34+
*/
35+
export function toRuntimeResponse(error: unknown): RuntimeErrorResponse {
36+
try {
37+
if (util.types.isNativeError(error) || isError(error)) {
38+
if (!error.stack) {
39+
throw new Error("Error stack is missing.");
40+
}
41+
return {
42+
errorType: error.name,
43+
errorMessage: error.message,
44+
trace: error.stack.split("\n") || [],
45+
};
46+
} else {
47+
return {
48+
errorType: typeof error,
49+
errorMessage: (error as any).toString(),
50+
trace: [],
51+
};
52+
}
53+
} catch (_err) {
54+
return {
55+
errorType: "handled",
56+
errorMessage:
57+
"callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack",
58+
trace: [],
59+
};
60+
}
61+
}
62+
63+
/**
64+
* Format an error with the expected properties.
65+
* For compatability, the error string always starts with a tab.
66+
*/
67+
export const toFormatted = (error: unknown): string => {
68+
try {
69+
return (
70+
"\t" + JSON.stringify(error, (_k, v) => _withEnumerableProperties(v))
71+
);
72+
} catch (err) {
73+
return "\t" + JSON.stringify(toRuntimeResponse(error));
74+
}
75+
};
76+
77+
/**
78+
* Error name, message, code, and stack are all members of the superclass, which
79+
* means they aren't enumerable and don't normally show up in JSON.stringify.
80+
* This method ensures those interesting properties are available along with any
81+
* user-provided enumerable properties.
82+
*/
83+
function _withEnumerableProperties(error: any) {
84+
if (error instanceof Error) {
85+
const extendedError: ExtendedError = <ExtendedError>(<any>error);
86+
const ret: any = Object.assign(
87+
{
88+
errorType: extendedError.name,
89+
errorMessage: extendedError.message,
90+
code: extendedError.code,
91+
},
92+
extendedError
93+
);
94+
if (typeof extendedError.stack == "string") {
95+
ret.stack = extendedError.stack.split("\n");
96+
}
97+
return ret;
98+
} else {
99+
return error;
100+
}
101+
}
102+
103+
export class ExtendedError extends Error {
104+
code?: number;
105+
custom?: string;
106+
reason?: string;
107+
promise?: Promise<any>;
108+
109+
constructor(reason?: string) {
110+
super(reason); // 'Error' breaks prototype chain here
111+
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
112+
}
113+
}
114+
115+
export class ImportModuleError extends ExtendedError {}
116+
export class HandlerNotFound extends ExtendedError {}
117+
export class MalformedHandlerName extends ExtendedError {}
118+
export class UserCodeSyntaxError extends ExtendedError {}
119+
export class UnhandledPromiseRejection extends ExtendedError {
120+
constructor(reason?: string, promise?: Promise<any>) {
121+
super(reason);
122+
this.reason = reason;
123+
this.promise = promise;
124+
}
125+
}
126+
127+
const errorClasses = [
128+
ImportModuleError,
129+
HandlerNotFound,
130+
MalformedHandlerName,
131+
UserCodeSyntaxError,
132+
UnhandledPromiseRejection,
133+
];
134+
135+
errorClasses.forEach((e) => {
136+
e.prototype.name = `Runtime.${e.name}`;
137+
});

src/runtime/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { load } from "./user-function";

src/runtime/user-function.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* Modifictions copyright 2021 Datadog, Inc.
4+
*
5+
* This module defines the functions for loading the user's code as specified
6+
* in a handler string.
7+
*/
8+
9+
"use strict";
10+
11+
import path from "path";
12+
import fs from "fs";
13+
import {
14+
HandlerNotFound,
15+
MalformedHandlerName,
16+
ImportModuleError,
17+
UserCodeSyntaxError,
18+
ExtendedError,
19+
} from "./errors";
20+
21+
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
22+
const RELATIVE_PATH_SUBSTRING = "..";
23+
24+
/**
25+
* Break the full handler string into two pieces, the module root and the actual
26+
* handler string.
27+
* Given './somepath/something/module.nestedobj.handler' this returns
28+
* ['./somepath/something', 'module.nestedobj.handler']
29+
*/
30+
function _moduleRootAndHandler(fullHandlerString: string): [string, string] {
31+
const handlerString = path.basename(fullHandlerString);
32+
const moduleRoot = fullHandlerString.substring(
33+
0,
34+
fullHandlerString.indexOf(handlerString)
35+
);
36+
return [moduleRoot, handlerString];
37+
}
38+
39+
/**
40+
* Split the handler string into two pieces: the module name and the path to
41+
* the handler function.
42+
*/
43+
function _splitHandlerString(handler: string): [string, string] {
44+
const match = handler.match(FUNCTION_EXPR);
45+
if (!match || match.length != 3) {
46+
throw new MalformedHandlerName("Bad handler");
47+
}
48+
return [match[1], match[2]]; // [module, function-path]
49+
}
50+
51+
/**
52+
* Resolve the user's handler function from the module.
53+
*/
54+
function _resolveHandler(object: any, nestedProperty: string): any {
55+
return nestedProperty.split(".").reduce((nested, key) => {
56+
return nested && nested[key];
57+
}, object);
58+
}
59+
60+
/**
61+
* Verify that the provided path can be loaded as a file per:
62+
* https://nodejs.org/dist/latest-v10.x/docs/api/modules.html#modules_all_together
63+
* @param string - the fully resolved file path to the module
64+
* @return bool
65+
*/
66+
function _canLoadAsFile(modulePath: string): boolean {
67+
return fs.existsSync(modulePath) || fs.existsSync(modulePath + ".js");
68+
}
69+
70+
/**
71+
* Attempt to load the user's module.
72+
* Attempts to directly resolve the module relative to the application root,
73+
* then falls back to the more general require().
74+
*/
75+
function _tryRequire(appRoot: string, moduleRoot: string, module: string): any {
76+
const lambdaStylePath = path.resolve(appRoot, moduleRoot, module);
77+
if (_canLoadAsFile(lambdaStylePath)) {
78+
return require(lambdaStylePath);
79+
} else {
80+
// Why not just require(module)?
81+
// Because require() is relative to __dirname, not process.cwd()
82+
const nodeStylePath = require.resolve(module, {
83+
paths: [appRoot, moduleRoot],
84+
});
85+
return require(nodeStylePath);
86+
}
87+
}
88+
89+
/**
90+
* Load the user's application or throw a descriptive error.
91+
* @throws Runtime errors in two cases
92+
* 1 - UserCodeSyntaxError if there's a syntax error while loading the module
93+
* 2 - ImportModuleError if the module cannot be found
94+
*/
95+
function _loadUserApp(
96+
appRoot: string,
97+
moduleRoot: string,
98+
module: string
99+
): any {
100+
try {
101+
return _tryRequire(appRoot, moduleRoot, module);
102+
} catch (e) {
103+
if (e instanceof SyntaxError) {
104+
throw new UserCodeSyntaxError(<any>e);
105+
// @ts-ignore
106+
} else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") {
107+
// @ts-ignore
108+
throw new ImportModuleError(e);
109+
} else {
110+
throw e;
111+
}
112+
}
113+
}
114+
115+
function _throwIfInvalidHandler(fullHandlerString: string): void {
116+
if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) {
117+
throw new MalformedHandlerName(
118+
`'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.`
119+
);
120+
}
121+
}
122+
123+
/**
124+
* Load the user's function with the approot and the handler string.
125+
* @param appRoot {string}
126+
* The path to the application root.
127+
* @param handlerString {string}
128+
* The user-provided handler function in the form 'module.function'.
129+
* @return userFuction {function}
130+
* The user's handler function. This function will be passed the event body,
131+
* the context object, and the callback function.
132+
* @throws In five cases:-
133+
* 1 - if the handler string is incorrectly formatted an error is thrown
134+
* 2 - if the module referenced by the handler cannot be loaded
135+
* 3 - if the function in the handler does not exist in the module
136+
* 4 - if a property with the same name, but isn't a function, exists on the
137+
* module
138+
* 5 - the handler includes illegal character sequences (like relative paths
139+
* for traversing up the filesystem '..')
140+
* Errors for scenarios known by the runtime, will be wrapped by Runtime.* errors.
141+
*/
142+
export const load = function (
143+
appRoot: string,
144+
fullHandlerString: string
145+
) {
146+
_throwIfInvalidHandler(fullHandlerString);
147+
148+
const [moduleRoot, moduleAndHandler] = _moduleRootAndHandler(
149+
fullHandlerString
150+
);
151+
const [module, handlerPath] = _splitHandlerString(moduleAndHandler);
152+
153+
const userApp = _loadUserApp(appRoot, moduleRoot, module);
154+
const handlerFunc = _resolveHandler(userApp, handlerPath);
155+
156+
if (!handlerFunc) {
157+
throw new HandlerNotFound(
158+
`${fullHandlerString} is undefined or not exported`
159+
);
160+
}
161+
162+
if (typeof handlerFunc !== "function") {
163+
throw new HandlerNotFound(`${fullHandlerString} is not a function`);
164+
}
165+
166+
return handlerFunc;
167+
};

0 commit comments

Comments
 (0)