Windshaft-cartodb/lib/api/middlewares/error-middleware.js

235 lines
6.8 KiB
JavaScript
Raw Normal View History

'use strict';
const _ = require('underscore');
const debug = require('debug')('windshaft:cartodb:error-middleware');
module.exports = function errorMiddleware (/* options */) {
return function error (err, req, res, next) {
// jshint unused:false
// jshint maxcomplexity:9
var allErrors = Array.isArray(err) ? err : [err];
allErrors = populateLimitErrors(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);
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);
}
2018-07-03 20:32:10 +08:00
function isTimeoutError (errorTypes) {
return errorTypes.renderTimeoutError || errorTypes.datasourceTimeoutError;
}
2019-10-22 01:07:24 +08:00
function getErrorTypes (error) {
2018-07-03 20:32:10 +08:00
return {
renderTimeoutError: isRenderTimeoutError(error),
2019-10-22 01:07:24 +08:00
datasourceTimeoutError: isDatasourceTimeoutError(error)
2018-07-03 20:32:10 +08:00
};
}
function isMaxWaitingClientsError (err) {
return err.message === 'max waitingClients count exceeded';
}
function populateLimitErrors (errors) {
return errors.map(function (error) {
if (isMaxWaitingClientsError(error)) {
2019-01-30 23:36:08 +08:00
error.message = 'You are over platform\'s limits: Max render capacity exceeded.' +
' Contact CARTO support for more details.';
error.type = 'limit';
2019-01-30 23:36:08 +08:00
error.subtype = 'render-capacity';
error.http_status = 429;
return error;
}
2018-07-03 20:32:10 +08:00
const errorTypes = getErrorTypes(error);
2018-07-03 20:32:10 +08:00
if (isTimeoutError(errorTypes)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
2018-07-03 20:32:10 +08:00
if (errorTypes.datasourceTimeoutError) {
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.';
}
2018-07-03 20:32:10 +08:00
if (errorTypes.renderTimeoutError) {
error.subtype = 'render';
error.message = 'You are over platform\'s limits: Render timeout error.' +
' Contact CARTO support for more details.';
}
return error;
});
}
2019-10-22 01:07:24 +08:00
function findStatusCode (err) {
var statusCode;
2019-10-22 01:07:24 +08:00
if (err.http_status) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
2019-10-22 01:07:24 +08:00
function statusFromErrorMessage (errMsg) {
// Find an appropriate statusCode based on message
// jshint maxcomplexity:7
var statusCode = 400;
2019-10-22 01:07:24 +08:00
if (errMsg.indexOf('permission denied') !== -1) {
statusCode = 403;
2019-10-22 01:07:24 +08:00
} else if (errMsg.indexOf('authentication failed') !== -1) {
statusCode = 403;
2019-10-22 01:07:24 +08:00
} else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
2019-10-22 01:07:24 +08:00
} else if (errMsg.indexOf('does not exist') !== -1) {
if (errMsg.indexOf(' role ') !== -1) {
statusCode = 403; // role 'xxx' does not exist
2019-10-22 01:07:24 +08:00
} else if (errMsg.match(/function .* does not exist/)) {
statusCode = 400; // invalid SQL (SQL function does not exist)
} else {
statusCode = 404;
}
}
return statusCode;
}
2019-10-22 01:07:24 +08:00
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;
2019-10-22 01:07:24 +08:00
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];
}
2019-10-22 01:07:24 +08:00
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',
2019-10-22 01:07:24 +08:00
message: stripConnectionInfo(message)
};
for (var prop in err) {
// type & message are properties from Error's prototype and will be skipped
if (Object.prototype.hasOwnProperty.call(err, prop) && shouldBeExposed(prop)) {
error[prop] = err[prop];
}
}
return error;
}
2017-11-25 00:53:07 +08:00
2019-10-22 01:07:24 +08:00
function setErrorHeader (errors, statusCode, res) {
const errorsCopy = errors.slice(0);
const mainError = errorsCopy.shift();
2017-11-25 00:53:07 +08:00
2019-10-22 01:07:24 +08:00
const errorsLog = {
2017-11-29 01:22:55 +08:00
mainError: {
statusCode: statusCode || 200,
2019-10-22 01:07:24 +08:00
message: mainError.message,
name: mainError.name,
label: mainError.label,
type: mainError.type,
subtype: mainError.subtype
2017-11-29 01:22:55 +08:00
}
2017-11-25 01:06:17 +08:00
};
2017-11-25 00:53:07 +08:00
errorsLog.moreErrors = errorsCopy.map(error => {
2017-11-25 00:53:07 +08:00
return {
message: error.message,
2019-10-22 01:07:24 +08:00
name: error.name,
label: error.label,
type: error.type,
2017-11-25 00:53:07 +08:00
subtype: error.subtype
};
});
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
/**
* Remove problematic nested characters
2017-12-18 18:14:27 +08:00
* from object for logs RegEx
*
* @param {Object} object
2017-12-18 18:14:27 +08:00
*/
2019-10-22 01:07:24 +08:00
function stringifyForLogs (object) {
2017-12-18 18:14:27 +08:00
Object.keys(object).map(key => {
2019-10-22 01:07:24 +08:00
if (typeof object[key] === 'string') {
2017-12-18 21:54:36 +08:00
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) {
2019-10-22 01:07:24 +08:00
for (const element of object[key]) {
2017-12-18 19:34:56 +08:00
stringifyForLogs(element);
}
2017-12-18 18:14:27 +08:00
}
});
return JSON.stringify(object);
2017-12-18 19:59:44 +08:00
}