@mojaloop/central-services-shared 18.30.7 → 18.30.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +9 -9
- package/src/util/request.js +53 -0
- package/test/unit/util/request.test.js +244 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [18.30.8](https://github.com/mojaloop/central-services-shared/compare/v18.30.7...v18.30.8) (2025-09-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* issue 4198 503 error handling ([#474](https://github.com/mojaloop/central-services-shared/issues/474)) ([8974f81](https://github.com/mojaloop/central-services-shared/commit/8974f81a5a3ee478d8014cff82f3329f51b9c654))
|
|
11
|
+
|
|
5
12
|
### [18.30.7](https://github.com/mojaloop/central-services-shared/compare/v18.30.6...v18.30.7) (2025-08-08)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/central-services-shared",
|
|
3
|
-
"version": "18.30.
|
|
3
|
+
"version": "18.30.8",
|
|
4
4
|
"description": "Shared code for mojaloop central services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "ModusBox",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@hapi/catbox": "12.1.1",
|
|
67
67
|
"@hapi/catbox-memory": "5.0.1",
|
|
68
|
-
"@hapi/hapi": "21.4.
|
|
68
|
+
"@hapi/hapi": "21.4.3",
|
|
69
69
|
"@hapi/joi-date": "2.0.1",
|
|
70
70
|
"@mojaloop/inter-scheme-proxy-cache-lib": "2.6.0",
|
|
71
71
|
"@opentelemetry/api": "1.9.0",
|
|
@@ -80,10 +80,10 @@
|
|
|
80
80
|
"fast-safe-stringify": "2.1.1",
|
|
81
81
|
"immutable": "5.1.3",
|
|
82
82
|
"ioredis": "5.7.0",
|
|
83
|
-
"joi": "18.0.
|
|
83
|
+
"joi": "18.0.1",
|
|
84
84
|
"lodash": "4.17.21",
|
|
85
85
|
"mustache": "4.2.0",
|
|
86
|
-
"openapi-backend": "5.
|
|
86
|
+
"openapi-backend": "5.15.0",
|
|
87
87
|
"raw-body": "3.0.0",
|
|
88
88
|
"rc": "1.2.8",
|
|
89
89
|
"redlock": "5.0.0-beta.2",
|
|
@@ -95,11 +95,11 @@
|
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@mojaloop/central-services-error-handling": "13.1.0",
|
|
98
|
-
"@mojaloop/central-services-logger": "11.9.
|
|
98
|
+
"@mojaloop/central-services-logger": "11.9.1",
|
|
99
99
|
"@mojaloop/central-services-metrics": "12.6.0",
|
|
100
100
|
"@mojaloop/event-sdk": "14.6.1",
|
|
101
|
-
"@mojaloop/sdk-standard-components": "19.16.
|
|
102
|
-
"@opentelemetry/auto-instrumentations-node": "^0.62.
|
|
101
|
+
"@mojaloop/sdk-standard-components": "19.16.7",
|
|
102
|
+
"@opentelemetry/auto-instrumentations-node": "^0.62.1",
|
|
103
103
|
"@types/hapi__joi": "17.1.15",
|
|
104
104
|
"ajv": "^8.17.1",
|
|
105
105
|
"ajv-formats": "^3.0.1",
|
|
@@ -107,13 +107,13 @@
|
|
|
107
107
|
"audit-ci": "7.1.0",
|
|
108
108
|
"base64url": "3.0.1",
|
|
109
109
|
"chance": "1.1.13",
|
|
110
|
-
"npm-check-updates": "18.0.
|
|
110
|
+
"npm-check-updates": "18.0.3",
|
|
111
111
|
"nyc": "17.1.0",
|
|
112
112
|
"portfinder": "1.0.37",
|
|
113
113
|
"pre-commit": "1.2.2",
|
|
114
114
|
"proxyquire": "2.1.3",
|
|
115
115
|
"replace": "1.2.2",
|
|
116
|
-
"rewire": "9.0.
|
|
116
|
+
"rewire": "9.0.1",
|
|
117
117
|
"sinon": "21.0.0",
|
|
118
118
|
"standard": "17.1.2",
|
|
119
119
|
"standard-version": "9.5.0",
|
package/src/util/request.js
CHANGED
|
@@ -162,6 +162,59 @@ const sendRequest = async ({
|
|
|
162
162
|
status: error.response?.status,
|
|
163
163
|
data: error.response?.data
|
|
164
164
|
})
|
|
165
|
+
|
|
166
|
+
// Check if this is an HTTP response error (4xx or 5xx) vs a network/connection error
|
|
167
|
+
if (error.response) {
|
|
168
|
+
// For HTTP errors, check if the response contains FSPIOP error information
|
|
169
|
+
const responseData = error.response.data
|
|
170
|
+
|
|
171
|
+
// If the response contains a valid errorInformation object, use it directly
|
|
172
|
+
if (responseData?.errorInformation) {
|
|
173
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPErrorFromErrorInformation(responseData.errorInformation)
|
|
174
|
+
if (sendRequestSpan) {
|
|
175
|
+
const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
|
|
176
|
+
await sendRequestSpan.error(fspiopError, state)
|
|
177
|
+
await sendRequestSpan.finish(fspiopError.message, state)
|
|
178
|
+
}
|
|
179
|
+
histTimerEnd({ success: false, source, destination, method })
|
|
180
|
+
throw fspiopError
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// For other 4xx errors without errorInformation, create appropriate FSPIOP error
|
|
184
|
+
if (error.response.status >= 400 && error.response.status < 500) {
|
|
185
|
+
let errorCode = ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR
|
|
186
|
+
let errorMessage = 'Client error'
|
|
187
|
+
|
|
188
|
+
// Map specific HTTP status codes to FSPIOP error codes
|
|
189
|
+
if (error.response.status === 400) {
|
|
190
|
+
errorCode = ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR
|
|
191
|
+
errorMessage = responseData?.message || 'Bad Request'
|
|
192
|
+
} else if (error.response.status === 404) {
|
|
193
|
+
errorCode = ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND
|
|
194
|
+
errorMessage = responseData?.message || 'The requested resource could not be found'
|
|
195
|
+
} else if (error.response.status === 403) {
|
|
196
|
+
errorCode = ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR
|
|
197
|
+
errorMessage = responseData?.message || 'Permission denied'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const extensions = [
|
|
201
|
+
{ key: 'url', value: url },
|
|
202
|
+
{ key: 'status', value: error.response.status },
|
|
203
|
+
{ key: 'response', value: stringify(responseData) }
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(errorCode, errorMessage, error, source, extensions)
|
|
207
|
+
if (sendRequestSpan) {
|
|
208
|
+
const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
|
|
209
|
+
await sendRequestSpan.error(fspiopError, state)
|
|
210
|
+
await sendRequestSpan.finish(fspiopError.message, state)
|
|
211
|
+
}
|
|
212
|
+
histTimerEnd({ success: false, source, destination, method })
|
|
213
|
+
throw fspiopError
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// For network errors or 5xx errors, use DESTINATION_COMMUNICATION_ERROR
|
|
165
218
|
const extensionArray = [
|
|
166
219
|
{ key: 'url', value: url },
|
|
167
220
|
{ key: 'sourceFsp', value: source },
|
|
@@ -73,6 +73,111 @@ Test('ParticipantEndpoint Model Test', modelTest => {
|
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
modelTest.test('sendRequest should', async (getEndpointTest) => {
|
|
76
|
+
// Factory for creating spans with common setup
|
|
77
|
+
const createSpan = (overrides = {}) => {
|
|
78
|
+
const spanFinishStub = sandbox.stub()
|
|
79
|
+
const spanAuditStub = sandbox.stub()
|
|
80
|
+
const spanErrorStub = sandbox.stub()
|
|
81
|
+
const sendRequestSpan = {
|
|
82
|
+
setTags: sandbox.stub(),
|
|
83
|
+
finish: spanFinishStub,
|
|
84
|
+
error: spanErrorStub,
|
|
85
|
+
...overrides.sendRequestSpan
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
span: {
|
|
89
|
+
getChild: sandbox.stub().returns(sendRequestSpan),
|
|
90
|
+
getContext: sandbox.stub().returns({ service: 'test' }),
|
|
91
|
+
injectContextToHttpRequest: sandbox.stub(requestOptions => requestOptions),
|
|
92
|
+
audit: spanAuditStub,
|
|
93
|
+
...overrides.span
|
|
94
|
+
},
|
|
95
|
+
stubs: { spanFinishStub, spanAuditStub, spanErrorStub, sendRequestSpan }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Factory for creating request options
|
|
100
|
+
const createRequestOptions = (fsp = 'fsp1', method = 'get') => ({
|
|
101
|
+
url: Mustache.render(Config.ENDPOINT_SOURCE_URL + Enum.EndPoints.FspEndpointTemplates.PARTICIPANT_ENDPOINTS_GET, { fsp }),
|
|
102
|
+
method
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Generic sendRequest caller
|
|
106
|
+
const callSendRequest = async (options = {}) => {
|
|
107
|
+
const { url, headers, source = hubName, destination = hubName, ...rest } = options
|
|
108
|
+
return Model.sendRequest({
|
|
109
|
+
url: url || createRequestOptions().url,
|
|
110
|
+
headers: headers || Helper.defaultHeaders(hubName, Enum.Http.HeaderResources.PARTICIPANTS, hubName),
|
|
111
|
+
source,
|
|
112
|
+
destination,
|
|
113
|
+
hubNameRegex,
|
|
114
|
+
...rest
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Helper function to test error handling scenarios
|
|
119
|
+
const testErrorHandling = async (test, errorConfig, expectations) => {
|
|
120
|
+
const { fsp = 'fsp1', errorMessage, errorCode, responseData } = errorConfig
|
|
121
|
+
const customError = new Error(errorMessage)
|
|
122
|
+
customError.code = errorCode
|
|
123
|
+
if (responseData) customError.response = responseData
|
|
124
|
+
|
|
125
|
+
request = sandbox.stub().throws(customError)
|
|
126
|
+
Model = proxyquire('../../../src/util/request', { axios: request })
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await callSendRequest({ url: createRequestOptions(fsp).url })
|
|
130
|
+
test.fail('should throw error')
|
|
131
|
+
} catch (e) {
|
|
132
|
+
test.ok(e instanceof Error, 'Error was thrown')
|
|
133
|
+
Object.entries(expectations).forEach(([key, value]) => {
|
|
134
|
+
if (key === 'notEqual') {
|
|
135
|
+
test.notEqual(e.apiErrorCode.code, value.code, value.message)
|
|
136
|
+
} else if (key === 'apiErrorCode') {
|
|
137
|
+
test.equal(e.apiErrorCode.code, value, expectations.apiErrorCodeMessage || 'Error code matches expected')
|
|
138
|
+
} else if (key === 'message') {
|
|
139
|
+
test.equal(e.message, value, expectations.messageDescription || 'Error message matches expected')
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
test.end()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Helper function to test span string payload handling
|
|
147
|
+
const testSpanStringPayload = async (test, payloadStr, expectedPayload, description) => {
|
|
148
|
+
const { span, stubs } = createSpan()
|
|
149
|
+
request = sandbox.stub().returns(Helper.getEndPointsResponse)
|
|
150
|
+
Model = proxyquire('../../../src/util/request', { axios: request })
|
|
151
|
+
|
|
152
|
+
await callSendRequest({ method: 'post', payload: payloadStr, span })
|
|
153
|
+
|
|
154
|
+
test.ok(stubs.spanAuditStub.calledOnce, 'Span audit is called')
|
|
155
|
+
const auditCallArgs = stubs.spanAuditStub.getCall(0).args[0]
|
|
156
|
+
const assertion = typeof expectedPayload === 'object' ? 'deepEqual' : 'equal'
|
|
157
|
+
test[assertion](auditCallArgs.payload, expectedPayload, description)
|
|
158
|
+
test.end()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Helper function to test error handling with span
|
|
162
|
+
const testErrorWithSpan = async (test, errorConfig, expectedCode) => {
|
|
163
|
+
const { span, stubs } = createSpan()
|
|
164
|
+
request = sandbox.stub().throws(errorConfig)
|
|
165
|
+
Model = proxyquire('../../../src/util/request', { axios: request })
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await callSendRequest({ span })
|
|
169
|
+
test.fail('should throw error')
|
|
170
|
+
} catch (e) {
|
|
171
|
+
test.ok(stubs.spanErrorStub.called, 'Span error is called')
|
|
172
|
+
test.ok(stubs.spanFinishStub.called, 'Span finish is called')
|
|
173
|
+
if (stubs.spanErrorStub.callCount > 0) {
|
|
174
|
+
const errorArg = stubs.spanErrorStub.getCall(0).args[0]
|
|
175
|
+
test.equal(errorArg.apiErrorCode.code, expectedCode, `Error code ${expectedCode} passed to span`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
test.end()
|
|
179
|
+
}
|
|
180
|
+
|
|
76
181
|
getEndpointTest.test('return the object of endpoints', async (test) => {
|
|
77
182
|
const fsp = 'fsp'
|
|
78
183
|
const requestOptions = {
|
|
@@ -293,6 +398,80 @@ Test('ParticipantEndpoint Model Test', modelTest => {
|
|
|
293
398
|
}
|
|
294
399
|
})
|
|
295
400
|
|
|
401
|
+
getEndpointTest.test('preserve 400 error with errorInformation from downstream service', async (test) => {
|
|
402
|
+
await testErrorHandling(test, {
|
|
403
|
+
fsp: 'nonexistentfsp',
|
|
404
|
+
errorMessage: 'Request failed with status code 400',
|
|
405
|
+
errorCode: 'ERR_BAD_REQUEST',
|
|
406
|
+
responseData: {
|
|
407
|
+
status: 400,
|
|
408
|
+
data: {
|
|
409
|
+
errorInformation: {
|
|
410
|
+
errorCode: '3200',
|
|
411
|
+
errorDescription: 'FSP not found'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}, {
|
|
416
|
+
apiErrorCode: '3200',
|
|
417
|
+
apiErrorCodeMessage: 'Error code is preserved from errorInformation',
|
|
418
|
+
message: 'FSP not found',
|
|
419
|
+
messageDescription: 'Error message is preserved from errorInformation',
|
|
420
|
+
notEqual: { code: '1001', message: 'Error is not converted to DESTINATION_COMMUNICATION_ERROR' }
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
getEndpointTest.test('handle 404 error without errorInformation', async (test) => {
|
|
425
|
+
await testErrorHandling(test, {
|
|
426
|
+
fsp: 'notfound',
|
|
427
|
+
errorMessage: 'Request failed with status code 404',
|
|
428
|
+
errorCode: 'ERR_BAD_REQUEST',
|
|
429
|
+
responseData: {
|
|
430
|
+
status: 404,
|
|
431
|
+
data: {
|
|
432
|
+
message: 'Resource not found'
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}, {
|
|
436
|
+
apiErrorCode: '3200',
|
|
437
|
+
apiErrorCodeMessage: 'Error code is ID_NOT_FOUND for 404',
|
|
438
|
+
message: 'Resource not found',
|
|
439
|
+
messageDescription: 'Error message is preserved',
|
|
440
|
+
notEqual: { code: '1001', message: 'Error is not converted to DESTINATION_COMMUNICATION_ERROR' }
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
getEndpointTest.test('handle network error as DESTINATION_COMMUNICATION_ERROR', async (test) => {
|
|
445
|
+
await testErrorHandling(test, {
|
|
446
|
+
errorMessage: 'ECONNREFUSED',
|
|
447
|
+
errorCode: 'ECONNREFUSED'
|
|
448
|
+
// No response property for network errors
|
|
449
|
+
}, {
|
|
450
|
+
apiErrorCode: '1001',
|
|
451
|
+
apiErrorCodeMessage: 'Network error is converted to DESTINATION_COMMUNICATION_ERROR',
|
|
452
|
+
message: 'Failed to send HTTP request to host',
|
|
453
|
+
messageDescription: 'Generic error message for network errors'
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
getEndpointTest.test('handle 500 error as DESTINATION_COMMUNICATION_ERROR', async (test) => {
|
|
458
|
+
await testErrorHandling(test, {
|
|
459
|
+
errorMessage: 'Request failed with status code 500',
|
|
460
|
+
errorCode: 'ERR_BAD_RESPONSE',
|
|
461
|
+
responseData: {
|
|
462
|
+
status: 500,
|
|
463
|
+
data: {
|
|
464
|
+
message: 'Internal Server Error'
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}, {
|
|
468
|
+
apiErrorCode: '1001',
|
|
469
|
+
apiErrorCodeMessage: '5xx error is converted to DESTINATION_COMMUNICATION_ERROR',
|
|
470
|
+
message: 'Failed to send HTTP request to host',
|
|
471
|
+
messageDescription: 'Generic error message for server errors'
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
296
475
|
getEndpointTest.test('sign with JWS signature when JwsSigner object is passed', async (test) => {
|
|
297
476
|
const fsp = 'payerfsp'
|
|
298
477
|
const payeefsp = 'payeefsp'
|
|
@@ -359,6 +538,71 @@ Test('ParticipantEndpoint Model Test', modelTest => {
|
|
|
359
538
|
test.end()
|
|
360
539
|
})
|
|
361
540
|
|
|
541
|
+
getEndpointTest.test('handle span with string payload that can be parsed as JSON', async (test) => {
|
|
542
|
+
await testSpanStringPayload(test, '{"test": "data"}', { test: 'data' }, 'String payload is parsed to JSON for audit')
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
getEndpointTest.test('handle span with string payload that cannot be parsed as JSON', async (test) => {
|
|
546
|
+
await testSpanStringPayload(test, 'not valid json', 'not valid json', 'String payload remains as string when not parseable')
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
getEndpointTest.test('handle 403 error without errorInformation', async (test) => {
|
|
550
|
+
await testErrorHandling(test, {
|
|
551
|
+
fsp: 'forbidden',
|
|
552
|
+
errorMessage: 'Request failed with status code 403',
|
|
553
|
+
responseData: {
|
|
554
|
+
status: 403,
|
|
555
|
+
data: { message: 'You do not have permission' }
|
|
556
|
+
}
|
|
557
|
+
}, {
|
|
558
|
+
apiErrorCode: '3000',
|
|
559
|
+
message: 'You do not have permission'
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
getEndpointTest.test('handle 400 error without errorInformation and without message', async (test) => {
|
|
564
|
+
await testErrorHandling(test, {
|
|
565
|
+
fsp: 'badrequest',
|
|
566
|
+
errorMessage: 'Request failed with status code 400',
|
|
567
|
+
responseData: {
|
|
568
|
+
status: 400,
|
|
569
|
+
data: {}
|
|
570
|
+
}
|
|
571
|
+
}, {
|
|
572
|
+
apiErrorCode: '3000',
|
|
573
|
+
message: 'Bad Request'
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
getEndpointTest.test('handle error with errorInformation and span', async (test) => {
|
|
578
|
+
const customError = new Error('Request failed with status code 400')
|
|
579
|
+
customError.response = {
|
|
580
|
+
status: 400,
|
|
581
|
+
data: {
|
|
582
|
+
errorInformation: {
|
|
583
|
+
errorCode: '3200',
|
|
584
|
+
errorDescription: 'FSP not found'
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
await testErrorWithSpan(test, customError, '3200')
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
getEndpointTest.test('handle 4xx error without errorInformation and with span', async (test) => {
|
|
592
|
+
const customError = new Error('Request failed with status code 403')
|
|
593
|
+
customError.response = {
|
|
594
|
+
status: 403,
|
|
595
|
+
data: { message: 'Forbidden' }
|
|
596
|
+
}
|
|
597
|
+
await testErrorWithSpan(test, customError, '3000')
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
getEndpointTest.test('handle network error with span', async (test) => {
|
|
601
|
+
const customError = new Error('ECONNREFUSED')
|
|
602
|
+
customError.code = 'ECONNREFUSED'
|
|
603
|
+
await testErrorWithSpan(test, customError, '1001')
|
|
604
|
+
})
|
|
605
|
+
|
|
362
606
|
getEndpointTest.end()
|
|
363
607
|
})
|
|
364
608
|
|