Skip to content
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

Improve error reporting #9499

Merged
merged 13 commits into from
May 19, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/aws/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ const getServiceInstance = memoize(
}
);

const normalizerPattern = /(?<!^)([A-Z])/g;
const normalizeErrorCodePostfix = (name) => {
return name.replace(normalizerPattern, '_$1').toUpperCase();
};

/** Execute request to AWS service
* @param {Object|string} [service] - Description of the service to call
* @prop [service.name] - Name of the service to call, support subclasses
Expand Down Expand Up @@ -198,13 +203,17 @@ async function awsRequest(service, method, ...args) {
providerError: Object.assign({}, err, { retryable: false }),
});
}
const providerErrorCodeExtension = err.code ? normalizeErrorCodePostfix(err.code) : 'ERROR';
throw Object.assign(
new ServerlessError(
process.env.SLS_DEBUG && err.stack ? `${err.stack}\n${'-'.repeat(100)}` : message,
err.code
`AWS_${normalizeErrorCodePostfix(service.name)}_${normalizeErrorCodePostfix(
method
)}_${providerErrorCodeExtension}`
),
{
providerError: err,
providerErrorCodeExtension,
}
);
}
Expand Down
23 changes: 15 additions & 8 deletions lib/classes/PluginManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -372,24 +372,31 @@ class PluginManager {
};
}
if (!_.isObject(options)) {
throw new Error(
throw new ServerlessError(
`Custom variable resolver {${variablePrefix}: ${JSON.stringify(
options
)}} defined by ${pluginName} isn't an object`
)}} defined by ${pluginName} isn't an object`,
'CUSTOM_LEGACY_VARIABLES_RESOLVER_INVALID_FORMAT'
);
} else if (!variablePrefix.match(/[0-9a-zA-Z_-]+/)) {
throw new Error(
`Custom variable resolver prefix ${variablePrefix} defined by ${pluginName} may only contain alphanumeric characters, hyphens or underscores.`
throw new ServerlessError(
`Custom variable resolver prefix ${variablePrefix} defined by ${pluginName} ` +
'may only contain alphanumeric characters, hyphens or underscores.',
'CUSTOM_LEGACY_VARIABLES_RESOLVER_INVALID_PREFIX'
);
}
if (typeof options.resolver !== 'function') {
throw new Error(
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} specifies a resolver that isn't a function: ${options.resolver}`
throw new ServerlessError(
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} ` +
`specifies a resolver that isn't a function: ${options.resolver}`,
'CUSTOM_LEGACY_VARIABLES_RESOLVER_INVALID_FORMAT'
);
}
if (options.isDisabledAtPrepopulation && typeof options.serviceName !== 'string') {
throw new Error(
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} specifies isDisabledAtPrepopulation but doesn't provide a string for a name`
throw new ServerlessError(
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} ` +
"specifies isDisabledAtPrepopulation but doesn't provide a string for a name",
'CUSTOM_LEGACY_VARIABLES_RESOLVER_INVALID_CONFIGURATION'
);
}
this.serverless.variables.variableResolvers.push({
Expand Down
5 changes: 4 additions & 1 deletion lib/classes/Service.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ class Service {
if (typeof value === 'object') {
return _.merge(memo, value);
}
throw new Error(`Non-object value specified in ${key} array: ${value}`);
throw new ServerlessError(
`Non-object value specified in ${key} array: ${value}`,
'LEGACY_CONFIGURATION_PROPERTY_MERGE_INVALID_INPUT'
);
}

return memo;
Expand Down
4 changes: 3 additions & 1 deletion lib/cli/handle-error.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const writeMessage = (title, message) => {
consoleLog(' ');
};

const isErrorCodeNormative = RegExp.prototype.test.bind(/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/);

module.exports = async (exception, options = {}) => {
if (!isObject(options)) options = {};
// Due to the fact that the handler can be invoked via fallback, we need to support both `serverless`
Expand Down Expand Up @@ -167,7 +169,7 @@ module.exports = async (exception, options = {}) => {
code: exception.code,
};

if (!isUserError || !exception.code) {
if (!isUserError || !exception.code || !isErrorCodeNormative(exception.code)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call with including location for such errors as well

failureReason.location = resolveErrorLocation(exceptionTokens);
}
await storeTelemetryLocally({ ...telemetryPayload, failureReason, outcome: 'failure' });
Expand Down
30 changes: 25 additions & 5 deletions lib/configuration/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,47 @@ const resolveTsNode = async (serviceDir) => {
try {
return (await resolveModulePath(__dirname, 'ts-node')).realPath;
} catch (slsDepError) {
if (slsDepError.code !== 'MODULE_NOT_FOUND') throw slsDepError;
if (slsDepError.code !== 'MODULE_NOT_FOUND') {
throw new ServerlessError(
`Cannot resolve "ts-node" due to: ${slsDepError.message}`,
'TS_NODE_RESOLUTION_ERROR'
);
}

// 2. If installed in a service, use it
try {
return (await resolveModulePath(serviceDir, 'ts-node')).realPath;
} catch (serviceDepError) {
if (serviceDepError.code !== 'MODULE_NOT_FOUND') throw serviceDepError;
if (serviceDepError.code !== 'MODULE_NOT_FOUND') {
throw new ServerlessError(
`Cannot resolve "ts-node" due to: ${serviceDepError.message}`,
'TS_NODE_IN_SERVICE_RESOLUTION_ERROR'
);
}

// 3. If installed globally, use it
const { stdoutBuffer } = await (async () => {
try {
return await spawn('npm', ['root', '-g']);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
throw new Error('"ts-node" not found');
if (error.code !== 'ENOENT') {
throw new ServerlessError(
`Cannot resolve "ts-node" due to unexpected "npm" error: ${error.message}`,
'TS_NODE_NPM_RESOLUTION_ERROR'
);
}
throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND');
}
})();
try {
return require.resolve(`${String(stdoutBuffer).trim()}/ts-node`);
} catch (globalDepError) {
if (globalDepError.code !== 'MODULE_NOT_FOUND') throw globalDepError;
if (globalDepError.code !== 'MODULE_NOT_FOUND') {
throw new ServerlessError(
`Cannot resolve "ts-node" due to: ${globalDepError.message}`,
'TS_NODE_NPM_GLOBAL_RESOLUTION_ERROR'
);
}
throw new ServerlessError('"ts-node" not found', 'TS_NODE_NOT_FOUND');
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/configuration/variables/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

const ensureString = require('type/string/ensure');
const ensurePlainObject = require('type/plain-object/ensure');
const ServerlessError = require('../../serverless-error');
const resolveMeta = require('./resolve-meta');
const resolve = require('./resolve');

Expand All @@ -23,7 +24,7 @@ const reportEventualErrors = (variablesMeta) => {

if (!resolutionErrors.size) return;

throw new Error(
throw new ServerlessError(
`Variables resolution errored with:${Array.from(
resolutionErrors,
(error) => `\n - ${error.message}`
Expand Down
4 changes: 3 additions & 1 deletion lib/plugins/aws/deploy/lib/validateTemplate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

const getCompiledTemplateS3Suffix = require('../../lib/naming').getCompiledTemplateS3Suffix;
const getS3EndpointForRegion = require('../../utils/getS3EndpointForRegion');
const ServerlessError = require('../../../../serverless-error');

module.exports = {
async validateTemplate() {
Expand All @@ -17,7 +19,7 @@ module.exports = {
const errorMessage = ['The CloudFormation template is invalid:', ` ${error.message}`].join(
''
);
throw new Error(errorMessage);
throw new ServerlessError(errorMessage, 'INVALID_AWS_CLOUDFORMATION_TEMPLATE');
});
},
};
5 changes: 4 additions & 1 deletion lib/plugins/aws/deployFunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ class AwsDeployFunction {
const roleResource = this.serverless.service.resources.Resources[role];

if (roleResource.Type !== 'AWS::IAM::Role') {
throw new Error('Provided resource is not IAM Role.');
throw new ServerlessError(
'Provided resource is not IAM Role',
'ROLE_REFERENCES_NON_AWS_IAM_ROLE'
);
}
const roleProperties = roleResource.Properties;
if (!roleProperties.RoleName) {
Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/aws/invoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class AwsInvoke {
}

if (invocationReply.FunctionError) {
throw new Error('Invoked function failed');
throw new ServerlessError('Invoked function failed', 'AWS_LAMBDA_INVOCATION_FAILED');
}
}

Expand Down
47 changes: 39 additions & 8 deletions lib/plugins/aws/invokeLocal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ class AwsInvokeLocal {
} else if (value.Ref) {
configuredEnvVars[name] = await resolveCfRefValue(this.provider, value.Ref);
} else {
throw new Error(`Unsupported format: ${inspect(value)}`);
throw new ServerlessError(
`Unsupported environment variable format: ${inspect(value)}`,
'INVOKE_LOCAL_UNSUPPORTED_ENV_VARIABLE'
);
}
} catch (error) {
throw new ServerlessError(
Expand Down Expand Up @@ -282,7 +285,10 @@ class AwsInvokeLocal {
try {
return await spawnExt('docker', ['version']);
} catch {
throw new Error('Please start the Docker daemon to use the invoke local Docker integration.');
throw new ServerlessError(
'Please start the Docker daemon to use the invoke local Docker integration.',
'DOCKER_DAEMON_NOT_FOUND'
);
}
}

Expand Down Expand Up @@ -410,7 +416,10 @@ class AwsInvokeLocal {
if (err.stdBuffer) {
process.stdout.write(err.stdBuffer);
}
throw new Error(`Failed to build docker image (exit code ${err.code}})`);
throw new ServerlessError(
`Failed to build docker image (exit code ${err.code}})`,
'DOCKER_IMAGE_BUILD_FAILED'
);
}
}

Expand Down Expand Up @@ -518,7 +527,10 @@ class AwsInvokeLocal {
if (err.stdBuffer) {
process.stdout.write(err.stdBuffer);
}
throw new Error(`Failed to run docker for ${runtime} image (exit code ${err.code}})`);
throw new ServerlessError(
`Failed to run docker for ${runtime} image (exit code ${err.code}})`,
'DOCKER_IMAGE_RUN_FAILED'
);
}
}

Expand Down Expand Up @@ -625,7 +637,10 @@ class AwsInvokeLocal {
try {
await fse.stat(artifactPath);
} catch {
throw new Error(`Artifact ${artifactPath} doesn't exists, please compile it first.`);
throw new ServerlessError(
`Artifact ${artifactPath} doesn't exists, please compile it first.`,
'JAVA_ARTIFACT_NOT_FOUND'
);
}
const timeout =
Number(this.options.functionObj.timeout) ||
Expand Down Expand Up @@ -685,7 +700,10 @@ class AwsInvokeLocal {
}
isRejected = true;
reject(
new Error(`Failed to build the Java bridge. exit code=${code} signal=${signal}`)
new ServerlessError(
`Failed to build the Java bridge. exit code=${code} signal=${signal}`,
'JAVA_BRIDGE_BUILD_FAILED'
)
);
}
});
Expand Down Expand Up @@ -746,7 +764,17 @@ class AwsInvokeLocal {
lambda = handlersContainer[handlerName];
} catch (error) {
this.serverless.cli.consoleLog(chalk.red(inspect(error)));
throw new Error(`Exception encountered when loading ${pathToHandler}`);
throw new ServerlessError(
`Exception encountered when loading ${pathToHandler}`,
'INVOKE_LOCAL_LAMBDA_INITIALIZATION_FAILED'
);
}

if (typeof lambda !== 'function') {
throw new ServerlessError(
`Lambda handler "${handlerPath}" does not point function property`,
'INVALID_LAMBDA_HANDLER_PATH'
);
}

function handleError(err) {
Expand Down Expand Up @@ -778,7 +806,10 @@ class AwsInvokeLocal {
body: JSON.parse(result.body),
});
} catch (e) {
throw new Error('Content-Type of response is application/json but body is not json');
throw new ServerlessError(
'Content-Type of response is application/json but body is not json',
'INVOKE_LOCAL_RESPONSE_TYPE_MISMATCH'
);
}
}
}
Expand Down
25 changes: 23 additions & 2 deletions lib/plugins/aws/lib/monitorStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ const ServerlessError = require('../../../serverless-error');

const validStatuses = new Set(['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'DELETE_COMPLETE']);

const normalizerPattern = /(?<!^)([A-Z])/g;
const resourceTypePattern = /^(?<domain>[^:]+)::(?<service>[^:]+)(?:::(?<method>.+))?$/;
const resourceTypeToErrorCodePostfix = (resourceType) => {
const { domain, service, method } = resourceType.match(resourceTypePattern).groups;
if (domain !== 'AWS') return `_${domain.replace(normalizerPattern, '_$1').toUpperCase()}`;
return `_${service.replace(normalizerPattern, '_$1')}_${method.replace(
normalizerPattern,
'_$1'
)}`.toUpperCase();
};

module.exports = {
async checkStackProgress(
action,
Expand Down Expand Up @@ -91,7 +102,17 @@ module.exports = {
let errorMessage = 'An error occurred: ';
errorMessage += `${stackLatestError.LogicalResourceId} - `;
errorMessage += `${stackLatestError.ResourceStatusReason}.`;
throw new ServerlessError(errorMessage, 'STACK_OPERATION_FAILURE');
const errorCode = (() => {
if (
stackLatestError.ResourceStatusReason.startsWith('Properties validation failed')
) {
return `AWS_STACK_${action.toUpperCase()}_VALIDATION_ERROR`;
}
return `AWS_STACK_${action.toUpperCase()}${resourceTypeToErrorCodePostfix(
stackLatestError.ResourceType
)}_${stackLatestError.ResourceStatus}`;
})();
throw new ServerlessError(errorMessage, errorCode);
}
},
(e) => {
Expand All @@ -102,7 +123,7 @@ module.exports = {
stackStatus = 'DELETE_COMPLETE';
return;
}
throw new ServerlessError(e.message, 'STACK_OPERATION_FAILURE');
throw e;
}
)
)
Expand Down
10 changes: 7 additions & 3 deletions lib/plugins/aws/lib/normalizeFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ module.exports = {
WebsocketsDeployment: normalizedTemplate.Resources[key],
})[key];
}
if (key === 'WebsocketsDeploymentStage') {
if (key === 'WebsocketsDeploymentStage' && _.get(value.Properties, 'DeploymentId')) {
const newVal = value;
newVal.Properties.DeploymentId.Ref = 'WebsocketsDeployment';
}
if (value.Type && value.Type === 'AWS::Lambda::Function') {
if (value.Type && value.Type === 'AWS::Lambda::Function' && _.get(value.Properties, 'Code')) {
const newVal = value;
newVal.Properties.Code.S3Key = '';
}
if (value.Type && value.Type === 'AWS::Lambda::LayerVersion') {
if (
value.Type &&
value.Type === 'AWS::Lambda::LayerVersion' &&
_.get(value.Properties, 'Content')
) {
const newVal = value;
newVal.Properties.Content.S3Key = '';
}
Expand Down