<commit_before>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 = populateTimeoutErrors(allErrors);

        const label = err.label || 'UNKNOWN';
        err = allErrors[0] || new Error(label);
        allErrors[0] = err;

        var statusCode = findStatusCode(err);

        logErrors(allErrors, statusCode, res);

        if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
            statusCode = 204;
        }

        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';
        }

        if (isDatasourceTimeoutError(error)) {
            error.subtype = 'datasource';
        }

        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;
        }

        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;
        }
    }


    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;
}

function logErrors(errors, statusCode, res) {
    const errorsCopy = Object.assign({}, errors);

    if(!errorsCopy || !errorsCopy.length) {
        return;
    }

    const mainError = errorsCopy.shift();

    const errorsLog = {
        statusCode: statusCode || 200,
        message:    mainError.message,
        name:       mainError.name,
        type:       mainError.type,
        subtype:    mainError.subtype
    };

    errorsLog.moreErrors = errorsCopy.map(error => {
        return {
            message: error.message,
            name:    error.name,
            type:    error.type,
            subtype: error.subtype
        };
    });

    res.set('X-Tiler-Errors', JSON.stringify(errorsLog));
}
<commit_msg>moving logErrors to right position<commit_after>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 = populateTimeoutErrors(allErrors);

        const label = err.label || 'UNKNOWN';
        err = allErrors[0] || new Error(label);
        allErrors[0] = err;

        var statusCode = findStatusCode(err);

        if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
            statusCode = 204;
        }
        
        logErrors(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);
}

function isTimeoutError (err) {
    return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}

function populateTimeoutErrors (errors) {
    return errors.map(function (error) {
        if (isRenderTimeoutError(error)) {
            error.subtype = 'render';
        }

        if (isDatasourceTimeoutError(error)) {
            error.subtype = 'datasource';
        }

        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;
        }

        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;
        }
    }


    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;
}

function logErrors(errors, statusCode, res) {
    const errorsCopy = Object.assign({}, errors);

    if(!errorsCopy || !errorsCopy.length) {
        return;
    }

    const mainError = errorsCopy.shift();

    const errorsLog = {
        statusCode: statusCode || 200,
        message:    mainError.message,
        name:       mainError.name,
        type:       mainError.type,
        subtype:    mainError.subtype
    };

    errorsLog.moreErrors = errorsCopy.map(error => {
        return {
            message: error.message,
            name:    error.name,
            type:    error.type,
            subtype: error.subtype
        };
    });

    res.set('X-Tiler-Errors', JSON.stringify(errorsLog));
}

{'fraction_non_alphanumeric': 0.12617252161118264, 'fraction_numerical': 0.011587272392863711, 'mean_word_length': 2.333844267320662, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3942586', 'n_tokens_mistral': 3345, 'n_tokens_neox': 3187, 'n_words': 744}