UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

360 lines 13.6 kB
import { initializer, location } from 'knifecycle'; import { DEFAULT_DEBUG_NODE_ENVS, DEFAULT_STRINGIFIERS, } from '../libs/constants.js'; import miniquery from 'miniquery'; import { printStackTrace } from 'yerror'; export const DEFAULT_ERROR_URI = 'https://stackoverflow.com/search?q=%5Bwhook%5D+$code'; export const DEFAULT_HELP_URI = 'https://stackoverflow.com/questions/ask?tags=whook&title=How+to+debug+$code+whook+error+code'; export const DEFAULT_DEFAULT_ERROR_CODE = 'E_UNEXPECTED'; export const DEFAULT_ERRORS_DESCRIPTORS = { [DEFAULT_DEFAULT_ERROR_CODE]: { code: 'error', description: 'Got an unexpected error', status: 500, help: DEFAULT_HELP_URI, }, E_TRANSACTION_TIMEOUT: { code: 'error', description: 'The request processing timed out (timeout: "$0")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_STRINGIFYER_LACK: { code: 'router_misconfiguration', description: 'The router has no stringifyer declared for the response content type "$0" with status "$1.status"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNDECLARED_PATH_PARAMETER: { code: 'router_misconfiguration', description: 'The no path parameter declared for the node "$0" on the path "$1"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_PATH: { code: 'router_misconfiguration', description: 'The path "$0" must start with a slash', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NO_OPERATION_ID: { code: 'router_misconfiguration', description: 'No operation id for the path "$0" and the "$1"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NO_HANDLER: { code: 'router_misconfiguration', description: 'No handler declared for the operation id "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_OPEN_API: { code: 'router_misconfiguration', description: 'The Open API provided to the router seems invalid', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNDECLARED_SECURITY_SCHEME: { code: 'router_misconfiguration', description: 'An operation ("$1") mentions an undeclared security scheme ("$0")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_BODY_SCHEMA: { code: 'router_misconfiguration', description: 'An operation ("$0") has an invalid body schema ("$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNSUPPORTED_HTTP_SCHEME: { code: 'router_misconfiguration', description: 'This router do not support this security schema yet ("$0")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNSUPPORTED_API_KEY_SOURCE: { code: 'router_misconfiguration', description: 'This router do not support this API key source yet ("$0")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_PARAMETER_NAME: { code: 'router_misconfiguration', description: 'Bad name for the parameter at index "$1" for the operation "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNSUPPORTED_PARAMETER_DEFINITION: { code: 'router_misconfiguration', description: 'This router does not support the parameter "$1" defined in "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_PARAMETER_SCHEMA: { code: 'router_misconfiguration', description: 'The router where unable to compile the parameter "$1" defined in "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NO_RESPONSE_PROMISE: { code: 'bad_handler', description: 'The handler for the operation id "$0" seems to not return a promise', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NO_RESPONSE: { code: 'bad_handler', description: 'The handler for the operation id "$0" seems to not return any response', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NO_RESPONSE_STATUS: { code: 'bad_handler', description: 'The handler for the operation id "$0" seems to not return any status', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NON_NUMERIC_STATUS: { code: 'bad_handler', description: 'The handler for the operation id "$0" seems to not be a number', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_NOT_FOUND: { code: 'bad_handler', description: 'No endpoint found at that path ("$0", "$2")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_REQUEST_CONTENT_TOO_LARGE: { code: 'bad_request', description: 'The request content length ("$0") exceeded the maximum allowed ("$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_BODY: { code: 'bad_request', description: 'The request body could not be parsed', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, transmittedParams: [0], }, E_BAD_BODY_LENGTH: { code: 'bad_request', description: 'The request body length ("$0") is different than the length defined in the headers ("$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNSUPPORTED_CHARSET: { code: 'bad_request', description: 'The body charset "$0" is not supported (supported char sets "$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_CONTENT_TYPE: { code: 'bad_request', description: 'The content type provided is weird "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_REQUEST_FAILURE: { code: 'bad_request', description: 'Failed to read the request body input stream', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNSUPPORTED_MEDIA_TYPE: { code: 'bad_request', description: 'The body media type "$0" is not supported (supported media types "$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNACCEPTABLE_CHARSET: { code: 'bad_request', description: 'The asked response charset "$0" is not satisfiable (allowed char sets "$2")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_UNACCEPTABLE_MEDIA_TYPE: { code: 'bad_request', description: 'The asked response media type "$0" is not satisfiable (allowed media types "$1")', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_REQUIRED_REQUEST_BODY: { code: 'bad_request', description: 'This endpoint requires a request body to be sent', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_REQUEST_BODY: { code: 'bad_request', description: 'The request body does not match the schema', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, transmittedParams: [2, 3], }, E_NO_REQUEST_BODY: { code: 'bad_request', description: 'This endpoint do not declare a request body but one were sent', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_REQUIRED_PARAMETER: { code: 'bad_request', description: 'This endpoint requires a value for the parameter "$0"', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_BAD_PARAMETER: { code: 'bad_request', description: 'The value provided for the parameter "$0" is not valid', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, transmittedParams: [2, 3], }, E_NON_REENTRANT_NUMBER: { code: 'bad_request', description: 'The value provided for the parameter "$0" is not reentrant', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, transmittedParams: [2, 3], }, E_BAD_BOOLEAN: { code: 'bad_request', description: 'The value provided ("$0") is not a boolean', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, E_TRANSACTION_ID_NOT_UNIQUE: { code: 'bad_request', description: 'The transaction id of the request "$0" is colliding with another one.', uri: DEFAULT_ERROR_URI, help: DEFAULT_HELP_URI, }, }; export const whookErrorSchema = { type: 'object', additionalProperties: false, properties: { error: { type: 'string' }, error_description: { type: 'string' }, error_uri: { type: 'string' }, error_debug_data: { type: 'object', additionalProperties: true, properties: { code: { type: 'string' }, stack: { type: 'string' }, // TODO: find a way to specify params or // maybe standardize it (only one object in params?) }, }, }, }; export default location(initializer({ name: 'errorHandler', type: 'service', inject: [ 'ENV', '?DEBUG_NODE_ENVS', '?STRINGIFIERS', '?ERRORS_DESCRIPTORS', '?DEFAULT_ERROR_CODE', ], }, initErrorHandler), import.meta.url); /** * Initialize an error handler for the * HTTP router * @param {Object} services * The services the server depends on * @param {Object} services.ENV * The app ENV * @param {Array} [services.DEBUG_NODE_ENVS] * The environnement that activate debugging * (prints stack trace in HTTP errors responses) * @param {Object} [services.STRINGIFIERS] * The synchronous body stringifiers * @param {Object} [services.ERRORS_DESCRIPTORS] * An hash of the various error descriptors * @param {Object} [services.DEFAULT_ERROR_CODE] * A string giving the default error code * @return {Promise} * A promise of a function to handle errors */ async function initErrorHandler({ ENV, DEBUG_NODE_ENVS = DEFAULT_DEBUG_NODE_ENVS, STRINGIFIERS = DEFAULT_STRINGIFIERS, ERRORS_DESCRIPTORS = DEFAULT_ERRORS_DESCRIPTORS, DEFAULT_ERROR_CODE = DEFAULT_DEFAULT_ERROR_CODE, }) { // Ensure we have required error descriptors ERRORS_DESCRIPTORS = { ...DEFAULT_ERRORS_DESCRIPTORS, ...ERRORS_DESCRIPTORS, }; return errorHandler; /** * Handle an HTTP transaction error and * map it to a serializable response * @param {String} transactionId * A raw NodeJS HTTP incoming message * @param {Object} responseSpec * The response specification * @param {YHTTPError} err * The encountered error * @return {Promise} * A promise resolving when the operation * completes */ async function errorHandler(transactionId, responseSpec, err) { const errorCode = err.code || DEFAULT_ERROR_CODE; const errorDescriptor = ERRORS_DESCRIPTORS[errorCode] || ERRORS_DESCRIPTORS[DEFAULT_DEFAULT_ERROR_CODE]; const response = { status: (errorDescriptor !== ERRORS_DESCRIPTORS[DEFAULT_DEFAULT_ERROR_CODE] && errorDescriptor.status ? errorDescriptor.status : undefined) || err.httpCode || ERRORS_DESCRIPTORS[DEFAULT_DEFAULT_ERROR_CODE].status || 500, headers: Object.assign({}, err.headers || {}, { // Avoid caching errors 'cache-control': 'private', // Fallback to the default stringifyer to always be // able to display errors 'content-type': responseSpec && responseSpec.contentTypes[0] && STRINGIFIERS[responseSpec.contentTypes[0]] ? responseSpec.contentTypes[0] : Object.keys(STRINGIFIERS)[0], }), // Here we do not respect the camelCase convention to // have OAuth2 compatible errors 🤷 body: { error: errorDescriptor.code, error_description: replaceTemplatedValues(err, errorDescriptor.description), error_uri: replaceTemplatedValues(err, errorDescriptor.uri), error_help_uri: replaceTemplatedValues(err, errorDescriptor.help), error_params: errorDescriptor.transmittedParams ? errorDescriptor.transmittedParams.map((paramIndex) => (err.params || [])[paramIndex]) : undefined, error_debug_data: { // Enjoy nerdy stuff: // https://en.wikipedia.org/wiki/Guru_Meditation guruMeditation: transactionId, }, }, }; if (DEBUG_NODE_ENVS.includes(ENV.NODE_ENV)) { response.body.error_debug_data.code = errorCode; response.body.error_debug_data.stack = printStackTrace(err); response.body.error_debug_data.params = err.params; } return response; } } function replaceTemplatedValues(err, str) { return str ? str .replace(/\$([0-9](:?\.[a-zA-Z0-9#@*]+)*)/g, (_, path) => miniquery(path, err.params ? [err.params] : []).join(', ')) .replace(/\$code/, err.code) : undefined; } //# sourceMappingURL=errorHandler.js.map