2017-09-21 17:46:31 +08:00
|
|
|
const _ = require('underscore');
|
|
|
|
const debug = require('debug')('windshaft:cartodb:error-middleware');
|
|
|
|
|
|
|
|
module.exports = function errorMiddleware (/* options */) {
|
2017-09-22 06:31:16 +08:00
|
|
|
return function error (err, req, res, next) {
|
2017-09-21 17:46:31 +08:00
|
|
|
// jshint unused:false
|
|
|
|
// jshint maxcomplexity:9
|
|
|
|
var allErrors = Array.isArray(err) ? err : [err];
|
|
|
|
|
|
|
|
allErrors = populateTimeoutErrors(allErrors);
|
|
|
|
|
|
|
|
const label = err.label || 'UNKNOWN';
|
|
|
|
err = allErrors[0] || new Error(label);
|
|
|
|
allErrors[0] = err;
|
|
|
|
|
|
|
|
var statusCode = findStatusCode(err);
|
|
|
|
|
2017-11-30 22:04:07 +08:00
|
|
|
setErrorHeader(allErrors, statusCode, res);
|
2017-09-21 17:46:31 +08:00
|
|
|
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
|
|
|
|
|
|
|
// If a callback was requested, force status to 200
|
|
|
|
if (req.query && req.query.callback) {
|
|
|
|
statusCode = 200;
|
|
|
|
}
|
|
|
|
|
|
|
|
var errorResponseBody = {
|
|
|
|
errors: allErrors.map(errorMessage),
|
|
|
|
errors_with_context: allErrors.map(errorMessageWithContext)
|
|
|
|
};
|
|
|
|
|
|
|
|
res.status(statusCode);
|
|
|
|
|
|
|
|
if (req.query && req.query.callback) {
|
|
|
|
res.jsonp(errorResponseBody);
|
|
|
|
} else {
|
|
|
|
res.json(errorResponseBody);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
function isRenderTimeoutError (err) {
|
|
|
|
return err.message === 'Render timed out';
|
|
|
|
}
|
|
|
|
|
|
|
|
function isDatasourceTimeoutError (err) {
|
|
|
|
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isTimeoutError (err) {
|
|
|
|
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
function populateTimeoutErrors (errors) {
|
|
|
|
return errors.map(function (error) {
|
|
|
|
if (isRenderTimeoutError(error)) {
|
|
|
|
error.subtype = 'render';
|
|
|
|
}
|
|
|
|
|
2018-07-02 19:02:36 +08:00
|
|
|
const IS_DATASOURCE_TIMEOUT_ERROR = isDatasourceTimeoutError(error);
|
|
|
|
|
2017-09-21 17:46:31 +08:00
|
|
|
if (isTimeoutError(error)) {
|
|
|
|
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
|
|
|
error.type = 'limit';
|
|
|
|
error.http_status = 429;
|
|
|
|
}
|
|
|
|
|
2018-07-02 19:02:36 +08:00
|
|
|
if (IS_DATASOURCE_TIMEOUT_ERROR) {
|
2018-06-29 21:01:12 +08:00
|
|
|
error.subtype = 'datasource';
|
|
|
|
error.message = 'You are over platform\'s limits: SQL query timeout error.' +
|
|
|
|
' Refactor your query before running again or contact CARTO support for more details.';
|
|
|
|
}
|
|
|
|
|
2017-09-21 17:46:31 +08:00
|
|
|
return error;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function findStatusCode(err) {
|
|
|
|
var statusCode;
|
|
|
|
if ( err.http_status ) {
|
|
|
|
statusCode = err.http_status;
|
|
|
|
} else {
|
|
|
|
statusCode = statusFromErrorMessage('' + err);
|
|
|
|
}
|
|
|
|
return statusCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports.findStatusCode = findStatusCode;
|
|
|
|
|
|
|
|
function statusFromErrorMessage(errMsg) {
|
|
|
|
// Find an appropriate statusCode based on message
|
|
|
|
// jshint maxcomplexity:7
|
|
|
|
var statusCode = 400;
|
|
|
|
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
|
|
|
statusCode = 403;
|
|
|
|
}
|
|
|
|
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
|
|
|
statusCode = 403;
|
|
|
|
}
|
|
|
|
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
|
|
|
statusCode = 400;
|
|
|
|
}
|
|
|
|
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
|
|
|
if ( -1 !== errMsg.indexOf(' role ') ) {
|
|
|
|
statusCode = 403; // role 'xxx' does not exist
|
|
|
|
} else if ( errMsg.match(/function .* does not exist/) ) {
|
|
|
|
statusCode = 400; // invalid SQL (SQL function does not exist)
|
|
|
|
} else {
|
|
|
|
statusCode = 404;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-22 06:31:16 +08:00
|
|
|
|
2017-09-21 17:46:31 +08:00
|
|
|
return statusCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
function errorMessage(err) {
|
|
|
|
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
|
|
|
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
|
|
|
|
|
|
|
return stripConnectionInfo(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports.errorMessage = errorMessage;
|
|
|
|
|
|
|
|
function stripConnectionInfo(message) {
|
|
|
|
// Strip connection info, if any
|
|
|
|
return message
|
|
|
|
// See https://github.com/CartoDB/Windshaft/issues/173
|
|
|
|
.replace(/Connection string: '[^']*'\n\s/im, '')
|
|
|
|
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
|
|
|
.replace(/is the server.*encountered/im, 'encountered');
|
|
|
|
}
|
|
|
|
|
|
|
|
var ERROR_INFO_TO_EXPOSE = {
|
|
|
|
message: true,
|
|
|
|
layer: true,
|
|
|
|
type: true,
|
|
|
|
analysis: true,
|
|
|
|
subtype: true
|
|
|
|
};
|
|
|
|
|
|
|
|
function shouldBeExposed (prop) {
|
|
|
|
return !!ERROR_INFO_TO_EXPOSE[prop];
|
|
|
|
}
|
|
|
|
|
|
|
|
function errorMessageWithContext(err) {
|
|
|
|
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
|
|
|
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
|
|
|
|
|
|
|
var error = {
|
|
|
|
type: err.type || 'unknown',
|
|
|
|
message: stripConnectionInfo(message),
|
|
|
|
};
|
|
|
|
|
|
|
|
for (var prop in err) {
|
|
|
|
// type & message are properties from Error's prototype and will be skipped
|
|
|
|
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
|
|
|
error[prop] = err[prop];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return error;
|
|
|
|
}
|
2017-11-25 00:53:07 +08:00
|
|
|
|
2017-11-30 22:04:07 +08:00
|
|
|
function setErrorHeader(errors, statusCode, res) {
|
2017-11-28 23:02:12 +08:00
|
|
|
let errorsCopy = errors.slice(0);
|
2017-11-27 23:43:04 +08:00
|
|
|
const mainError = errorsCopy.shift();
|
2017-11-25 00:53:07 +08:00
|
|
|
|
2017-11-28 23:02:12 +08:00
|
|
|
let errorsLog = {
|
2017-11-29 01:22:55 +08:00
|
|
|
mainError: {
|
|
|
|
statusCode: statusCode || 200,
|
|
|
|
message: mainError.message,
|
|
|
|
name: mainError.name,
|
|
|
|
label: mainError.label,
|
|
|
|
type: mainError.type,
|
|
|
|
subtype: mainError.subtype
|
|
|
|
}
|
2017-11-25 01:06:17 +08:00
|
|
|
};
|
2017-11-25 00:53:07 +08:00
|
|
|
|
2017-11-27 23:43:04 +08:00
|
|
|
errorsLog.moreErrors = errorsCopy.map(error => {
|
2017-11-25 00:53:07 +08:00
|
|
|
return {
|
|
|
|
message: error.message,
|
2017-11-27 23:43:04 +08:00
|
|
|
name: error.name,
|
2017-11-28 01:12:44 +08:00
|
|
|
label: error.label,
|
2017-11-25 00:53:07 +08:00
|
|
|
type: error.type,
|
|
|
|
subtype: error.subtype
|
|
|
|
};
|
|
|
|
});
|
2018-03-23 21:13:27 +08:00
|
|
|
|
2017-12-18 19:34:56 +08:00
|
|
|
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
|
2017-11-27 23:47:45 +08:00
|
|
|
}
|
2017-12-18 18:14:27 +08:00
|
|
|
|
|
|
|
/**
|
2018-03-23 21:13:27 +08:00
|
|
|
* Remove problematic nested characters
|
2017-12-18 18:14:27 +08:00
|
|
|
* from object for logs RegEx
|
2018-03-23 21:13:27 +08:00
|
|
|
*
|
|
|
|
* @param {Object} object
|
2017-12-18 18:14:27 +08:00
|
|
|
*/
|
|
|
|
function stringifyForLogs(object) {
|
|
|
|
Object.keys(object).map(key => {
|
2017-12-18 21:54:36 +08:00
|
|
|
if(typeof object[key] === 'string') {
|
|
|
|
object[key] = object[key].replace(/[^a-zA-Z0-9]/g, ' ');
|
|
|
|
} else if (typeof object[key] === 'object') {
|
2017-12-18 19:34:56 +08:00
|
|
|
stringifyForLogs(object[key]);
|
|
|
|
} else if (object[key] instanceof Array) {
|
|
|
|
for (let element of object[key]) {
|
|
|
|
stringifyForLogs(element);
|
|
|
|
}
|
2017-12-18 18:14:27 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return JSON.stringify(object);
|
2017-12-18 19:59:44 +08:00
|
|
|
}
|