New features:
- Automatic error reporting (enabled by default)
- Increase build times by leveraging docker build caches
- 
Fixes:
- Fix error handling
- Fix vue autodetect
- Custom dockerfile is not the default

Others:
- Cleanup `logs-servers` collection, because old errors are not standardized
- New Traefik proxy version
- Standardized directory configurations
This commit is contained in:
Andras Bacsai
2021-04-19 09:46:05 +02:00
committed by GitHub
parent bad84289c4
commit 142b83cc13
51 changed files with 1300 additions and 837 deletions

View File

@@ -0,0 +1,75 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleErrors = exports.handleValidationError = exports.handleNotFoundError = void 0;
const http_errors_enhanced_1 = require("http-errors-enhanced");
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils");
const validation_1 = require("./validation");
function handleNotFoundError(request, reply) {
handleErrors(new http_errors_enhanced_1.NotFoundError('Not found.'), request, reply);
}
exports.handleNotFoundError = handleNotFoundError;
function handleValidationError(error, request) {
/*
As seen in https://github.com/fastify/fastify/blob/master/lib/validation.js
the error.message will always start with the relative section (params, querystring, headers, body)
and fastify throws on first failing section.
*/
const section = error.message.match(/^\w+/)[0];
return new http_errors_enhanced_1.BadRequestError('One or more validations failed trying to process your request.', {
failedValidations: validation_1.convertValidationErrors(section, Reflect.get(request, section), error.validation)
});
}
exports.handleValidationError = handleValidationError;
function handleErrors(error, request, reply) {
var _a, _b;
// It is a generic error, handle it
const code = error.code;
if (!('statusCode' in error)) {
if ('validation' in error && ((_a = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _a === void 0 ? void 0 : _a.convertValidationErrors)) {
// If it is a validation error, convert errors to human friendly format
error = handleValidationError(error, request);
}
else if ((_b = request[interfaces_1.kHttpErrorsEnhancedConfiguration]) === null || _b === void 0 ? void 0 : _b.hideUnhandledErrors) {
// It is requested to hide the error, just log it and then create a generic one
request.log.error({ error: http_errors_enhanced_1.serializeError(error) });
error = new http_errors_enhanced_1.InternalServerError('An error occurred trying to process your request.');
}
else {
// Wrap in a HttpError, making the stack explicitily available
error = new http_errors_enhanced_1.InternalServerError(http_errors_enhanced_1.serializeError(error));
Object.defineProperty(error, 'stack', { enumerable: true });
}
}
else if (code === 'INVALID_CONTENT_TYPE' || code === 'FST_ERR_CTP_INVALID_MEDIA_TYPE') {
error = new http_errors_enhanced_1.UnsupportedMediaTypeError(utils_1.upperFirst(validation_1.validationMessagesFormatters.contentType()));
}
else if (code === 'FST_ERR_CTP_EMPTY_JSON_BODY') {
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.jsonEmpty()));
}
else if (code === 'MALFORMED_JSON' || error.message === 'Invalid JSON' || error.stack.includes('at JSON.parse')) {
error = new http_errors_enhanced_1.BadRequestError(utils_1.upperFirst(validation_1.validationMessagesFormatters.json()));
}
// Get the status code
let { statusCode, headers } = error;
// Code outside HTTP range
if (statusCode < 100 || statusCode > 599) {
statusCode = http_errors_enhanced_1.INTERNAL_SERVER_ERROR;
}
// Create the body
const body = {
statusCode,
error: http_errors_enhanced_1.messagesByCodes[statusCode],
message: error.message
};
http_errors_enhanced_1.addAdditionalProperties(body, error);
// Send the error back
// eslint-disable-next-line @typescript-eslint/no-floating-promises
reply
.code(statusCode)
.headers(headers !== null && headers !== void 0 ? headers : {})
.type('application/json')
.send(body);
}
exports.handleErrors = handleErrors;

View File

@@ -0,0 +1,58 @@
/* eslint-disable */
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugin = exports.validationMessagesFormatters = exports.niceJoin = exports.convertValidationErrors = void 0;
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
const handlers_1 = require("./handlers");
const interfaces_1 = require("./interfaces");
const validation_1 = require("./validation");
__exportStar(require("./handlers"), exports);
__exportStar(require("./interfaces"), exports);
var validation_2 = require("./validation");
Object.defineProperty(exports, "convertValidationErrors", { enumerable: true, get: function () { return validation_2.convertValidationErrors; } });
Object.defineProperty(exports, "niceJoin", { enumerable: true, get: function () { return validation_2.niceJoin; } });
Object.defineProperty(exports, "validationMessagesFormatters", { enumerable: true, get: function () { return validation_2.validationMessagesFormatters; } });
exports.plugin = fastify_plugin_1.default(function (instance, options, done) {
var _a, _b, _c, _d;
const isProduction = process.env.NODE_ENV === 'production';
const convertResponsesValidationErrors = (_a = options.convertResponsesValidationErrors) !== null && _a !== void 0 ? _a : !isProduction;
const configuration = {
hideUnhandledErrors: (_b = options.hideUnhandledErrors) !== null && _b !== void 0 ? _b : isProduction,
convertValidationErrors: (_c = options.convertValidationErrors) !== null && _c !== void 0 ? _c : true,
responseValidatorCustomizer: options.responseValidatorCustomizer,
allowUndeclaredResponses: (_d = options.allowUndeclaredResponses) !== null && _d !== void 0 ? _d : false
};
instance.decorate(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
instance.decorateRequest(interfaces_1.kHttpErrorsEnhancedConfiguration, null);
instance.addHook('onRequest', async (request) => {
request[interfaces_1.kHttpErrorsEnhancedConfiguration] = configuration;
});
instance.setErrorHandler(handlers_1.handleErrors);
// instance.setNotFoundHandler(handlers_1.handleNotFoundError);
if (convertResponsesValidationErrors) {
instance.decorate(interfaces_1.kHttpErrorsEnhancedResponseValidations, []);
instance.addHook('onRoute', validation_1.addResponseValidation);
instance.addHook('onReady', validation_1.compileResponseValidationSchema.bind(instance, configuration));
}
done();
}, { name: 'fastify-http-errors-enhanced' });
exports.default = exports.plugin;
// Fix CommonJS exporting
/* istanbul ignore else */
if (typeof module !== 'undefined') {
module.exports = exports.plugin;
Object.assign(module.exports, exports);
}

View File

@@ -0,0 +1,6 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.kHttpErrorsEnhancedResponseValidations = exports.kHttpErrorsEnhancedConfiguration = void 0;
exports.kHttpErrorsEnhancedConfiguration = Symbol('fastify-http-errors-enhanced-configuration');
exports.kHttpErrorsEnhancedResponseValidations = Symbol('fastify-http-errors-enhanced-response-validation');

View File

@@ -0,0 +1,31 @@
/* eslint-disable */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.get = exports.upperFirst = void 0;
function upperFirst(source) {
if (typeof source !== 'string' || !source.length) {
return source;
}
return source[0].toUpperCase() + source.substring(1);
}
exports.upperFirst = upperFirst;
function get(target, path) {
var _a;
const tokens = path.split('.').map((t) => t.trim());
for (const token of tokens) {
if (typeof target === 'undefined' || target === null) {
// We're supposed to be still iterating, but the chain is over - Return undefined
target = undefined;
break;
}
const index = token.match(/^(\d+)|(?:\[(\d+)\])$/);
if (index) {
target = target[parseInt((_a = index[1]) !== null && _a !== void 0 ? _a : index[2], 10)];
}
else {
target = target[token];
}
}
return target;
}
exports.get = get;

View File

@@ -0,0 +1,239 @@
/* eslint-disable */
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileResponseValidationSchema = exports.addResponseValidation = exports.convertValidationErrors = exports.validationMessagesFormatters = exports.niceJoin = void 0;
const ajv_1 = __importDefault(require("ajv"));
const http_errors_enhanced_1 = require("http-errors-enhanced");
const interfaces_1 = require("./interfaces");
const utils_1 = require("./utils");
function niceJoin(array, lastSeparator = ' and ', separator = ', ') {
switch (array.length) {
case 0:
return '';
case 1:
return array[0];
case 2:
return array.join(lastSeparator);
default:
return array.slice(0, array.length - 1).join(separator) + lastSeparator + array[array.length - 1];
}
}
exports.niceJoin = niceJoin;
exports.validationMessagesFormatters = {
contentType: () => 'only JSON payloads are accepted. Please set the "Content-Type" header to start with "application/json"',
json: () => 'the body payload is not a valid JSON',
jsonEmpty: () => 'the JSON body payload cannot be empty if the "Content-Type" header is set',
missing: () => 'must be present',
unknown: () => 'is not a valid property',
uuid: () => 'must be a valid GUID (UUID v4)',
timestamp: () => 'must be a valid ISO 8601 / RFC 3339 timestamp (example: 2018-07-06T12:34:56Z)',
date: () => 'must be a valid ISO 8601 / RFC 3339 date (example: 2018-07-06)',
time: () => 'must be a valid ISO 8601 / RFC 3339 time (example: 12:34:56)',
uri: () => 'must be a valid URI',
hostname: () => 'must be a valid hostname',
ipv4: () => 'must be a valid IPv4',
ipv6: () => 'must be a valid IPv6',
paramType: (type) => {
switch (type) {
case 'integer':
return 'must be a valid integer number';
case 'number':
return 'must be a valid number';
case 'boolean':
return 'must be a valid boolean (true or false)';
case 'object':
return 'must be a object';
case 'array':
return 'must be an array';
default:
return 'must be a string';
}
},
presentString: () => 'must be a non empty string',
minimum: (min) => `must be a number greater than or equal to ${min}`,
maximum: (max) => `must be a number less than or equal to ${max}`,
minimumProperties(min) {
return min === 1 ? 'cannot be a empty object' : `must be a object with at least ${min} properties`;
},
maximumProperties(max) {
return max === 0 ? 'must be a empty object' : `must be a object with at most ${max} properties`;
},
minimumItems(min) {
return min === 1 ? 'cannot be a empty array' : `must be an array with at least ${min} items`;
},
maximumItems(max) {
return max === 0 ? 'must be a empty array' : `must be an array with at most ${max} items`;
},
enum: (values) => `must be one of the following values: ${niceJoin(values.map((f) => `"${f}"`), ' or ')}`,
pattern: (pattern) => `must match pattern "${pattern.replace(/\(\?:/g, '(')}"`,
invalidResponseCode: (code) => `This endpoint cannot respond with HTTP status ${code}.`,
invalidResponse: (code) => `The response returned from the endpoint violates its specification for the HTTP status ${code}.`,
invalidFormat: (format) => `must match format "${format}" (format)`
};
function convertValidationErrors(section, data, validationErrors) {
const errors = {};
if (section === 'querystring') {
section = 'query';
}
// For each error
for (const e of validationErrors) {
let message = '';
let pattern;
let value;
let reason;
// Normalize the key
let key = e.dataPath;
if (key.startsWith('.')) {
key = key.substring(1);
}
// Remove useless quotes
/* istanbul ignore next */
if (key.startsWith('[') && key.endsWith(']')) {
key = key.substring(1, key.length - 1);
}
// Depending on the type
switch (e.keyword) {
case 'required':
case 'dependencies':
key = e.params.missingProperty;
message = exports.validationMessagesFormatters.missing();
break;
case 'additionalProperties':
key = e.params.additionalProperty;
message = exports.validationMessagesFormatters.unknown();
break;
case 'type':
message = exports.validationMessagesFormatters.paramType(e.params.type);
break;
case 'minProperties':
message = exports.validationMessagesFormatters.minimumProperties(e.params.limit);
break;
case 'maxProperties':
message = exports.validationMessagesFormatters.maximumProperties(e.params.limit);
break;
case 'minItems':
message = exports.validationMessagesFormatters.minimumItems(e.params.limit);
break;
case 'maxItems':
message = exports.validationMessagesFormatters.maximumItems(e.params.limit);
break;
case 'minimum':
message = exports.validationMessagesFormatters.minimum(e.params.limit);
break;
case 'maximum':
message = exports.validationMessagesFormatters.maximum(e.params.limit);
break;
case 'enum':
message = exports.validationMessagesFormatters.enum(e.params.allowedValues);
break;
case 'pattern':
pattern = e.params.pattern;
value = utils_1.get(data, key);
if (pattern === '.+' && !value) {
message = exports.validationMessagesFormatters.presentString();
}
else {
message = exports.validationMessagesFormatters.pattern(e.params.pattern);
}
break;
case 'format':
reason = e.params.format;
// Normalize the key
if (reason === 'date-time') {
reason = 'timestamp';
}
message = (exports.validationMessagesFormatters[reason] || exports.validationMessagesFormatters.invalidFormat)(reason);
break;
}
// No custom message was found, default to input one replacing the starting verb and adding some path info
if (!message.length) {
message = `${e.message.replace(/^should/, 'must')} (${e.keyword})`;
}
// Remove useless quotes
/* istanbul ignore next */
if (key.match(/(?:^['"])(?:[^.]+)(?:['"]$)/)) {
key = key.substring(1, key.length - 1);
}
// Fix empty properties
if (!key) {
key = '$root';
}
key = key.replace(/^\//, '');
errors[key] = message;
}
return { [section]: errors };
}
exports.convertValidationErrors = convertValidationErrors;
function addResponseValidation(route) {
var _a;
if (!((_a = route.schema) === null || _a === void 0 ? void 0 : _a.response)) {
return;
}
const validators = {};
/*
Add these validators to the list of the one to compile once the server is started.
This makes possible to handle shared schemas.
*/
this[interfaces_1.kHttpErrorsEnhancedResponseValidations].push([
this,
validators,
Object.entries(route.schema.response)
]);
// Note that this hook is not called for non JSON payloads therefore validation is not possible in such cases
route.preSerialization = async function (request, reply, payload) {
const statusCode = reply.raw.statusCode;
// Never validate error 500
if (statusCode === http_errors_enhanced_1.INTERNAL_SERVER_ERROR) {
return payload;
}
// No validator, it means the HTTP status is not allowed
const validator = validators[statusCode];
if (!validator) {
if (request[interfaces_1.kHttpErrorsEnhancedConfiguration].allowUndeclaredResponses) {
return payload;
}
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponseCode(statusCode));
}
// Now validate the payload
const valid = validator(payload);
if (!valid) {
throw new http_errors_enhanced_1.InternalServerError(exports.validationMessagesFormatters.invalidResponse(statusCode), {
failedValidations: convertValidationErrors('response', payload, validator.errors)
});
}
return payload;
};
}
exports.addResponseValidation = addResponseValidation;
function compileResponseValidationSchema(configuration) {
// Fix CJS/ESM interoperability
// @ts-expect-error
let AjvConstructor = ajv_1.default;
/* istanbul ignore next */
if (AjvConstructor.default) {
AjvConstructor = AjvConstructor.default;
}
const hasCustomizer = typeof configuration.responseValidatorCustomizer === 'function';
for (const [instance, validators, schemas] of this[interfaces_1.kHttpErrorsEnhancedResponseValidations]) {
// @ts-expect-error
const compiler = new AjvConstructor({
// The fastify defaults, with the exception of removeAdditional and coerceTypes, which have been reversed
removeAdditional: false,
useDefaults: true,
coerceTypes: false,
allErrors: true
});
compiler.addSchema(Object.values(instance.getSchemas()));
compiler.addKeyword('example');
if (hasCustomizer) {
configuration.responseValidatorCustomizer(compiler);
}
for (const [code, schema] of schemas) {
validators[code] = compiler.compile(schema);
}
}
}
exports.compileResponseValidationSchema = compileResponseValidationSchema;