UNPKG

18.5 kBJavaScriptView Raw
1/*****
2 License
3 --------------
4 Copyright © 2017 Bill & Melinda Gates Foundation
5 The Mojaloop files are made available by the Bill & Melinda Gates Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
11 Contributors
12 --------------
13 This is the official list of the Mojaloop project contributors for this file.
14 Names of the original copyright holders (individuals or organizations)
15 should be listed with a '*' in the first column. People who have
16 contributed from an organization can be listed under the organization
17 that actually holds the copyright for their contributions (see the
18 Gates Foundation organization for an example). Those individuals should have
19 their names indented and be marked with a '-'. Email address can be added
20 optionally within square brackets <email>.
21
22 * Gates Foundation
23 - Name Surname <name.surname@gatesfoundation.com>
24
25 * ModusBox
26 - Neal Donnan <neal.donnan@modusbox.com>
27 - Juan Correa <juan.correa@modusbox.com>
28 - Miguel de Barros <miguel.debarros@modusbox.com>
29
30 --------------
31 ******/
32
33'use strict'
34
35const Enums = require('./enums')
36const _ = require('lodash')
37const MojaloopFSPIOPError = require('@mojaloop/sdk-standard-components').Errors.MojaloopFSPIOPError
38
39/**
40 * See section 7.6 of "API Definition v1.0.docx". Note that some of the these
41 * error objects contain an httpStatusCode property that indicates the HTTP
42 * response code for cases where errors are returned immediately i.e. upon
43 * request, rather than on callback. Those error objects that do not contain
44 * an httpStatusCode property are expected to only be returned to callers in
45 * error callbacks after the initial request was accepted with a 202/200.
46 */
47class FSPIOPError extends MojaloopFSPIOPError {
48 /**
49 * Constructs a new error object
50 *
51 * @param cause {object} - Underlying error object or any type that represents the cause of this error
52 * @param message {string} - A friendly error message
53 * @param replyTo {string} - FSPID of the participant to whom this error is addressed
54 * @param apiErrorCode {object} - The MojaloopApiErrorCodes object representing the API spec error
55 * @param extensions {object} - API spec extensions object (if applicable)
56 * @param useMessageAsDescription {boolean} - Use the message as the Error description. This is useful when converting errorInformation objects into FSPIOPErrors.
57 */
58 constructor (cause, message, replyTo, apiErrorCode, extensions, useMessageAsDescription = false) {
59 // Validate incoming params (if required)
60 // TODO: Need to clarify ML API Specification for the correct model structure for the extensionList - catering for both scenarios until this can be clarified
61 if (extensions && !((Array.isArray(extensions)) || (extensions.extension && Array.isArray(extensions.extension)))) throw new Error('FSPIOPError Parameter Validation Failure - extensions is not a list or does not contain an extension list.')
62 if (!apiErrorCode.code && !apiErrorCode.message) throw new Error('FSPIOPError Parameter Validation Failure - apiErrorCode is not valid error code enum.')
63
64 // Constructor logic goes here
65 const clonedExtensions = _.cloneDeep(extensions) // makes sure we make a copy.
66 super(cause, message, replyTo, apiErrorCode, clonedExtensions)
67 this._setStackFromCause(cause)
68 this.useMessageAsDescription = useMessageAsDescription
69 }
70
71 /**
72 * Internal only method to set the stack trace based on the set Cause.
73 * This can be used to serialise the error to a JSON body.
74 *
75 * @param cause {object} - Underlying error object or any type that represents the cause of this error
76 */
77 _setStackFromCause (cause) {
78 let stringifiedCause
79 if (typeof this.cause === 'string' || this.cause instanceof String) {
80 stringifiedCause = this.cause
81 } else if (this.cause instanceof Error) {
82 stringifiedCause = this.cause.stack
83 } else {
84 stringifiedCause = JSON.stringify(this.cause)
85 }
86 if (stringifiedCause) this.stack = `${this.stack}\n${stringifiedCause}`
87 }
88
89 /**
90 * Returns an object that complies with the API specification for error bodies. By default object does not contain the cause extension
91 * This can be used to serialise the error to a JSON body
92 *
93 * @param includeCauseExtension {boolean} - Flag to specify whether or not to include cause extension at extension list
94 * @param truncateCause {boolean} - Flag to specify whether or not to truncate the cause string to match Mojaloop API v1.0 Spec
95 *
96 * @returns {object}
97 */
98 toApiErrorObject ({ includeCauseExtension = false, truncateExtensions = true } = {}) {
99 let errorDescription = this.apiErrorCode.message
100
101 // Lets check if the message is defined, not null or empty (i.e. undefined).
102 if ((this.message && this.message !== 'null' && this.message.length > 0) && !this.useMessageAsDescription) {
103 errorDescription = `${errorDescription} - ${this.message}`
104 } else if (this.useMessageAsDescription) { // Lets check to see if we must use the message as the errorDescription.
105 errorDescription = `${this.message}`
106 }
107
108 const e = {
109 errorInformation: {
110 errorCode: this.apiErrorCode.code,
111 errorDescription
112 }
113 }
114
115 if (this.extensions) {
116 e.errorInformation.extensionList = {}
117
118 if (Array.isArray(this.extensions)) {
119 // TODO: Need to clarify ML API Specification for the correct model structure for the extensionList - catering for both scenarios until this can be clarified
120 // e.errorInformation.extensionList = _.cloneDeep(this.extensions)
121 e.errorInformation.extensionList.extension = _.cloneDeep(this.extensions)
122 } else if (this.extensions.extension && Array.isArray(this.extensions.extension)) {
123 e.errorInformation.extensionList.extension = _.cloneDeep(this.extensions.extension)
124 }
125
126 if (includeCauseExtension === true) {
127 const causeKeyValueFromExtensions = e.errorInformation.extensionList.extension.find(keyValue => keyValue.key === Enums.Internal.FSPIOPError.ExtensionsKeys.cause)
128 if (causeKeyValueFromExtensions) {
129 causeKeyValueFromExtensions.value = `${this.stack}\n${causeKeyValueFromExtensions.value}`
130 } else {
131 const causeKeyValue = {
132 key: Enums.Internal.FSPIOPError.ExtensionsKeys.cause,
133 value: this.stack
134 }
135 e.errorInformation.extensionList.extension.push(causeKeyValue)
136 }
137 } else if (e.errorInformation.extensionList.extension && Array.isArray(e.errorInformation.extensionList.extension)) {
138 _.remove(e.errorInformation.extensionList.extension, (extensionKeyValue) => {
139 return [Enums.Internal.FSPIOPError.ExtensionsKeys.cause, Enums.Internal.FSPIOPError.ExtensionsKeys._cause].includes(extensionKeyValue.key)
140 })
141 if (e.errorInformation.extensionList.extension.length === 0) {
142 delete e.errorInformation.extensionList
143 }
144 }
145 } else {
146 if (includeCauseExtension === true) {
147 e.errorInformation.extensionList = {
148 extension: [{
149 key: Enums.Internal.FSPIOPError.ExtensionsKeys.cause,
150 value: this.stack
151 }]
152 }
153 }
154 }
155 const hasExtension = e.errorInformation.extensionList && e.errorInformation.extensionList.extension && e.errorInformation.extensionList.extension.length
156 if (truncateExtensions && hasExtension) {
157 for (const i in e.errorInformation.extensionList.extension) {
158 if (e.errorInformation.extensionList.extension[i].value) {
159 e.errorInformation.extensionList.extension[i].value = e.errorInformation.extensionList.extension[i].value.toString().substr(0, Enums.MojaloopModelTypes.ExtensionValue.constraints.max)
160 }
161 }
162 }
163
164 return e
165 }
166
167 toString () {
168 return JSON.stringify(this.toFullErrorObject())
169 }
170}
171
172/**
173 * Factory method to create a new FSPIOPError.
174 *
175 * @param apiErrorCode {object} - the FSPIOP Error enum
176 * @param message {string} - a description of the error
177 * @param cause {object/string} - the original Error
178 * @param replyTo {string} - the FSP to notify of the error
179 * @param extensions {object} - additional information to associate with the error
180 * @param useDescriptionAsMessage {boolean} - Enables concatinations of the Message & Error Description on the
181 * @returns {FSPIOPError} - create the specified error, will fall back to INTERNAL_SERVER_ERROR if the apiErrorCode is undefined
182 */
183const createFSPIOPError = (apiErrorCode, message, cause, replyTo, extensions, useDescriptionAsMessage = false) => {
184 if (apiErrorCode && apiErrorCode.code && apiErrorCode.message) {
185 const newApiError = Object.assign({}, apiErrorCode)
186 let match = Enums.findFSPIOPErrorCode(apiErrorCode.code)
187 if (!match) {
188 match = Enums.findErrorType(apiErrorCode.code)
189 if (!match) {
190 throw new FSPIOPError(cause, `Factory function createFSPIOPError failed due to apiErrorCode being invalid - ${JSON.stringify(apiErrorCode)}.`, replyTo, Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, extensions)
191 }
192 if (!newApiError.httpStatusCode) {
193 newApiError.httpStatusCode = match.httpStatusCode
194 }
195 } else if (!newApiError.httpStatusCode) {
196 newApiError.httpStatusCode = match.httpStatusCode
197 }
198 return new FSPIOPError(cause, message, replyTo, newApiError, extensions, useDescriptionAsMessage)
199 } else {
200 throw new FSPIOPError(cause, `Factory function createFSPIOPError failed due to apiErrorCode being invalid - ${JSON.stringify(apiErrorCode)}.`, replyTo, Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, extensions)
201 }
202}
203
204/**
205 * Factory method to create an FSPIOPError from a Joi error.
206 *
207 * @param error {Error} - the Joi error
208 * @param cause {object/string} - an Error to use as the cause of the error if available
209 * @param replyTo {string} - the FSP to notify of the error if applicable
210 * @returns {FSPIOPError}
211 */
212const createFSPIOPErrorFromJoiError = (error, cause, replyTo) => {
213 const fspiopError = ((type) => {
214 switch (type) {
215 case 'any.required':
216 case 'any.empty':
217 return Enums.FSPIOPErrorCodes.MISSING_ELEMENT
218 case 'object.allowUnknown':
219 return Enums.FSPIOPErrorCodes.TOO_MANY_ELEMENTS
220 // Match any type that starts with 'string.'
221 case (type.match(/^string\./) || {}).input:
222 case 'date.format':
223 case 'number.integer':
224 case 'any.allowOnly':
225 return Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX
226 default:
227 return Enums.FSPIOPErrorCodes.VALIDATION_ERROR
228 }
229 })(error.type)
230
231 const stackTrace = (cause && cause.stack)
232 ? cause.stack
233 : cause
234
235 const source = (
236 cause &&
237 cause.output &&
238 cause.output.payload &&
239 cause.output.payload.validation
240 )
241 ? cause.output.payload.validation.source
242 : undefined
243
244 const messages = {
245 header: `'${error.context.label}' HTTP header`,
246 params: `'${error.context.label}' URI path parameter`
247 }
248
249 // If the error was caused by a missing or invalid header or path parameter respond with
250 // appropriate text indicating as much
251 const msg = (source && messages[source])
252 ? messages[source]
253 : error.message
254
255 return createFSPIOPError(fspiopError, msg, stackTrace, replyTo)
256}
257
258/**
259 * Factory method to create an FSPIOPError from a openapi-backend error.
260 *
261 * @param error {Object} - the openapi error
262 * @param replyTo {string} - the FSP to notify of the error if applicable
263 * @returns {FSPIOPError}
264 */
265const createFSPIOPErrorFromOpenapiError = (error, replyTo) => {
266 const fspiopError = ((type) => {
267 switch (type) {
268 case 'required':
269 return Enums.FSPIOPErrorCodes.MISSING_ELEMENT
270 case 'additionalProperties':
271 return Enums.FSPIOPErrorCodes.TOO_MANY_ELEMENTS
272 case 'type':
273 return Enums.FSPIOPErrorCodes.MALFORMED_SYNTAX
274 case 'notFound':
275 return Enums.FSPIOPErrorCodes.UNKNOWN_URI
276 case 'methodNotAllowed':
277 return Enums.FSPIOPErrorCodes.METHOD_NOT_ALLOWED
278 default:
279 return Enums.FSPIOPErrorCodes.VALIDATION_ERROR
280 }
281 })(error.keyword)
282 let message
283 if (error.message) {
284 message = error.dataPath + ' ' + error.message
285 } else {
286 message = error.dataPath
287 }
288 return createFSPIOPError(fspiopError, message, replyTo)
289}
290
291/**
292 * Convenience factory method to create a FSPIOPError Internal Server Error
293 *
294 * @param message {string} - a description of the error
295 * @param cause {object/string} - the original Error
296 * @param replyTo {string} - the FSP to notify of the error if applicable
297 * @param extensions {object} - additional information to associate with the error
298 * @returns {FSPIOPError}
299 */
300const createInternalServerFSPIOPError = (message, cause, replyTo, extensions) => {
301 return createFSPIOPError(Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, message, cause, replyTo, extensions)
302}
303
304/**
305 * Factory method to reformat an FSPIOPError based on the erro being passed in.
306 * If the error passed in is an FSPIOPError it will be returned as is.
307 * If the error is any other error it will be wrapped in an FSPIOPError using the original error message
308 * and error stack trace.
309 *
310 * @param error the error to reformat
311 * @param apiErrorCode {object} - the FSPIOP Error enum, defaults to INTERNAL_SERVER_ERROR
312 * @param replyTo {string} - the FSP to notify of the error if applicable
313 * @param extensions {object} - additional information to associate with the error
314 * @returns {FSPIOPError}
315 */
316const reformatFSPIOPError = (error, apiErrorCode = Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, replyTo, extensions) => {
317 if (error.constructor && error.constructor.name === FSPIOPError.name) {
318 return error
319 } else {
320 return createFSPIOPError(apiErrorCode, error.message, error.stack, replyTo, extensions)
321 }
322}
323
324/**
325 * Factory method to create an FSPIOPError based on the errorInformation object being passed in.
326 *
327 * @param errorInformation {object} - Mojaloop JSON ErrorInformation object
328 * @param cause {object/string} - the original Error
329 * @param replyTo {string} - the FSP to notify of the error if applicable
330 * @returns {FSPIOPError}
331 */
332const createFSPIOPErrorFromErrorInformation = (errorInformation, cause, replyTo) => {
333 const errorCode = {
334 code: errorInformation.errorCode,
335 message: errorInformation.errorDescription
336 }
337 return createFSPIOPError(errorCode, errorInformation.errorDescription, cause, replyTo, errorInformation.extensionList, true)
338}
339
340/**
341 * Factory method to create an FSPIOPError based on an errorCode (string or number).
342 *
343 * @param code {string/number} - Mojaloop Spec error code in either a string or number.
344 * @param message {string} - a description of the error
345 * @param cause {object/string} - the original Error
346 * @param replyTo {string} - the FSP to notify of the error if applicable
347 * @param extensions {object} - additional information to associate with the error
348 * @returns {FSPIOPError}
349 */
350const createFSPIOPErrorFromErrorCode = (code, message, cause, replyTo, extensions) => {
351 const errorCode = validateFSPIOPErrorCode(code)
352 return createFSPIOPError(errorCode, message, cause, replyTo, extensions)
353}
354
355/**
356 * Validate a code against the Mojaloop API spec, returns the enum or throws an exception if invalid.
357 *
358 * @param code {number/string/object} - Mojaloop API spec error code (four digit integer as number or string or apiErrorCode enum)
359 * @param throwException {boolean} - Mojaloop API spec error code (four digit integer as number or string)
360 * @returns apiErrorCode {object} - if valid, false if not (unless throwException is true, then an exception will be thrown instead)
361 * @throws {FSPIOPError} - Internal Server Error indicating that the error code is invalid.
362 */
363const validateFSPIOPErrorCode = (code) => {
364 const errorMessage = 'Validation failed due to error code being invalid'
365 let codeToValidate
366 if (typeof code === 'number' || typeof code === 'string') { // check to see if this is a normal error code represented by a number or string
367 codeToValidate = code
368 } else if (typeof code === 'object' && code.code) { // check to see if this is a apiErrorCode error
369 codeToValidate = code.code
370 }
371 // validate the error code
372 const result = Enums.findFSPIOPErrorCode(codeToValidate)
373 if (result) {
374 return result
375 } else {
376 throw createInternalServerFSPIOPError(`${errorMessage} - ${JSON.stringify(code)}.`)
377 }
378}
379
380/**
381 * Validate a code against the Mojaloop API spec, specifically custom errors, returns the incoming error code or throws an exception if invalid.
382 *
383 * @param code {number/string/object} - Mojaloop API spec error code (four digit integer as number or string or apiErrorCode enum)
384 * @param throwException {boolean} - Mojaloop API spec error code (four digit integer as number or string)
385 * @returns boolean - if valid, true, if false then an exception will be thrown instead)
386 * @throws {FSPIOPError} - Internal Server Error indicating that the error code is invalid.
387 */
388const validateFSPIOPErrorGroups = (code) => {
389 const errorMessage = 'Validation failed due to error code being invalid'
390 let codeToValidate
391 if (typeof code === 'number' || typeof code === 'string') { // check to see if this is a normal error code represented by a number or string
392 codeToValidate = code
393 } else if (typeof code === 'object' && code.code) { // check to see if this is a apiErrorCode error
394 codeToValidate = code.code
395 }
396 // validate the custom error code
397 const regex = /^(10|20|3[0-4]|4[0-4]|5[0-4])[0-9]{2}$/
398 if (regex.test(codeToValidate)) {
399 return true
400 } else {
401 throw createInternalServerFSPIOPError(`${errorMessage} - ${JSON.stringify(code)}.`)
402 }
403}
404
405module.exports = {
406 FSPIOPError,
407 createFSPIOPError,
408 createFSPIOPErrorFromJoiError,
409 createFSPIOPErrorFromOpenapiError,
410 createInternalServerFSPIOPError,
411 createFSPIOPErrorFromErrorInformation,
412 createFSPIOPErrorFromErrorCode,
413 reformatFSPIOPError,
414 validateFSPIOPErrorCode,
415 validateFSPIOPErrorGroups
416}