'use strict'; const debug = require('debug')('windshaft:cartodb:error-middleware'); module.exports = function errorMiddleware (/* options */) { return function error (err, req, res, next) { const { logger } = res.locals; var allErrors = Array.isArray(err) ? err : [err]; allErrors = populateLimitErrors(allErrors); // TODO REMOVE THIS THREE LINES PLEASE const label = err.label || 'UNKNOWN'; err = allErrors[0] || new Error(label); allErrors[0] = err; var statusCode = findStatusCode(err); // If a callback was requested, force status to 200 if (req.query && req.query.callback) { statusCode = 200; } allErrors.forEach((err) => debug('[%s ERROR] -- %d: %s, %s', err.label || 'UNKNOWN', statusCode, err, err.stack)); logger.error({ errors: allErrors }); 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); } return next(); }; }; 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 (errorTypes) { return errorTypes.renderTimeoutError || errorTypes.datasourceTimeoutError; } function getErrorTypes (error) { return { renderTimeoutError: isRenderTimeoutError(error), datasourceTimeoutError: isDatasourceTimeoutError(error) }; } function isMaxWaitingClientsError (err) { return err.message === 'max waitingClients count exceeded'; } function populateLimitErrors (errors) { return errors.map(function (error) { if (isMaxWaitingClientsError(error)) { error.message = 'You are over platform\'s limits: Max render capacity exceeded.' + ' Contact CARTO support for more details.'; error.type = 'limit'; error.subtype = 'render-capacity'; error.http_status = 429; return error; } const errorTypes = getErrorTypes(error); 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; } if (errorTypes.datasourceTimeoutError) { 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.'; } 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; }); } 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 var statusCode = 400; if (errMsg.indexOf('permission denied') !== -1) { statusCode = 403; } else if (errMsg.indexOf('authentication failed') !== -1) { statusCode = 403; } else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) { statusCode = 400; } else if (errMsg.indexOf('does not exist') !== -1) { if (errMsg.indexOf(' role ') !== -1) { 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; } } return statusCode; } function errorMessage (err) { // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 var message = (typeof err === 'string' ? 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 = (typeof err === 'string' ? 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 (Object.prototype.hasOwnProperty.call(err, prop) && shouldBeExposed(prop)) { error[prop] = err[prop]; } } return error; }