@open-xchange/soap-client 0.1.5 → 0.1.6
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 +8 -1
- package/package.json +1 -1
- package/soap.js +28 -9
- package/test/soap.test.js +43 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.6] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Aborting a non-retryable SOAP fault no longer corrupts the error message. p-retry v8 passes `onFailedAttempt` a context object (`{ error, attemptNumber, retriesLeft, ... }`) rather than the error itself; the abort path was passing that context object to `AbortError`, which stored it verbatim as `.message`. p-retry rejects with that `AbortError` as-is (it only unwraps `AbortError`s thrown by the retried function, not by the callback), so consumers received an error whose `.message` was an object and crashed with `error.message.includes is not a function` — masking the real fault. We now pass the inner `context.error`, keeping `.message` a string and letting `wrapSoapFault` recover the fault downstream. Both retry sites (WSDL fetch and per-method calls) share one `handleFailedAttempt` helper.
|
|
12
|
+
|
|
7
13
|
## [0.1.5] - 2026-05-26
|
|
8
14
|
|
|
9
15
|
### Fixed
|
|
@@ -113,7 +119,8 @@ All notable changes to this project will be documented in this file.
|
|
|
113
119
|
|
|
114
120
|
- Initial release: extract SOAP client into its own library
|
|
115
121
|
|
|
116
|
-
[unreleased]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.
|
|
122
|
+
[unreleased]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.6...main
|
|
123
|
+
[0.1.6]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.5...soap-client-0.1.6
|
|
117
124
|
[0.1.5]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.4...soap-client-0.1.5
|
|
118
125
|
[0.1.4]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.3...soap-client-0.1.4
|
|
119
126
|
[0.1.3]: https://gitlab.com/openxchange/appsuite/web-foundation/tools/-/compare/soap-client-0.1.2...soap-client-0.1.3
|
package/package.json
CHANGED
package/soap.js
CHANGED
|
@@ -217,6 +217,31 @@ function shouldAbortRetry (error) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* p-retry v8 `onFailedAttempt` handler, shared by both retry sites below.
|
|
222
|
+
*
|
|
223
|
+
* IMPORTANT: p-retry v8 invokes `onFailedAttempt` with a *context object*
|
|
224
|
+
* `{ error, attemptNumber, retriesLeft, retriesConsumed, retryDelay }` — NOT
|
|
225
|
+
* the error itself (that was the pre-v7 signature). So when we abort, we must
|
|
226
|
+
* pass `context.error` (the real Error) to `AbortError`, not `context`.
|
|
227
|
+
*
|
|
228
|
+
* Why it matters: `new AbortError(nonError)` sets `AbortError.message` to the
|
|
229
|
+
* value verbatim, and p-retry does not unwrap an AbortError thrown *by this
|
|
230
|
+
* callback* — it rejects with it as-is. Passing `context` would therefore
|
|
231
|
+
* reject with an Error whose `.message` is an object, which later blows up
|
|
232
|
+
* consumers doing `error.message.includes(...)` (appsuite-codeceptjs
|
|
233
|
+
* contexts.js) with "error.message.includes is not a function", masking the
|
|
234
|
+
* real SOAP fault. Passing `context.error` keeps `.message` a string and lets
|
|
235
|
+
* `wrapSoapFault` recover the fault downstream.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} context p-retry attempt context.
|
|
238
|
+
* @param {string} label Human-readable label for the retry log line.
|
|
239
|
+
*/
|
|
240
|
+
function handleFailedAttempt (context, label) {
|
|
241
|
+
if (shouldAbortRetry(context)) throw new AbortError(context.error)
|
|
242
|
+
console.log(`Retrying ${label} in ${context.retriesLeft} attempts (${describeError(context)})`)
|
|
243
|
+
}
|
|
244
|
+
|
|
220
245
|
/**
|
|
221
246
|
* This function creates a SOAP client for the specified service type.
|
|
222
247
|
* @param {string} type The name of the service type.
|
|
@@ -241,10 +266,7 @@ async function createClientAsync (type) {
|
|
|
241
266
|
gzip: true
|
|
242
267
|
}), {
|
|
243
268
|
...RETRY_OPTIONS,
|
|
244
|
-
onFailedAttempt:
|
|
245
|
-
if (shouldAbortRetry(error)) throw new AbortError(error)
|
|
246
|
-
console.log(`Retrying WSDL fetch (${type}) in ${error.retriesLeft} attempts (${describeError(error)})`)
|
|
247
|
-
}
|
|
269
|
+
onFailedAttempt: context => handleFailedAttempt(context, `WSDL fetch (${type})`)
|
|
248
270
|
})
|
|
249
271
|
|
|
250
272
|
// https://stackoverflow.com/questions/30740415/namespace-for-array-field-in-node-soap-client-node-js
|
|
@@ -272,10 +294,7 @@ async function createClientAsync (type) {
|
|
|
272
294
|
try {
|
|
273
295
|
const result = await pRetry(() => origMethod.apply(this, [soapOptions, { timeout: 30000, ...clientOptions }, ...args]), {
|
|
274
296
|
...RETRY_OPTIONS,
|
|
275
|
-
onFailedAttempt:
|
|
276
|
-
if (shouldAbortRetry(error)) throw new AbortError(error)
|
|
277
|
-
console.log(`Retrying ${String(prop)} in ${error.retriesLeft} attempts (${describeError(error)})`)
|
|
278
|
-
}
|
|
297
|
+
onFailedAttempt: context => handleFailedAttempt(context, String(prop))
|
|
279
298
|
})
|
|
280
299
|
|
|
281
300
|
performance.mark(endMark)
|
|
@@ -294,4 +313,4 @@ async function createClientAsync (type) {
|
|
|
294
313
|
})
|
|
295
314
|
}
|
|
296
315
|
|
|
297
|
-
export { createClientAsync, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError }
|
|
316
|
+
export { createClientAsync, logSoapError, describeError, shouldAbortRetry, wrapSoapFault, unwrapError, handleFailedAttempt }
|
package/test/soap.test.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// We don't exercise that path here; set a stub so the import doesn't crash.
|
|
8
8
|
process.env.PROVISIONING_URL = 'http://soap.test/'
|
|
9
9
|
|
|
10
|
-
const { describeError, shouldAbortRetry, wrapSoapFault } = await import('../soap.js')
|
|
10
|
+
const { describeError, shouldAbortRetry, wrapSoapFault, handleFailedAttempt } = await import('../soap.js')
|
|
11
11
|
|
|
12
12
|
// Build a SOAP fault object matching the real shape (from soap@1.9.x).
|
|
13
13
|
function soapFault (faultstring, detail = {}) {
|
|
@@ -224,3 +224,45 @@ describe('real-world: transient "Could not acquire claim" fault wrapped in { err
|
|
|
224
224
|
expect(out.body).toMatch(/soap:Envelope/)
|
|
225
225
|
})
|
|
226
226
|
})
|
|
227
|
+
|
|
228
|
+
describe('handleFailedAttempt (p-retry v8 onFailedAttempt)', () => {
|
|
229
|
+
// p-retry v8 hands the callback a context object, not the error itself.
|
|
230
|
+
function attemptContext (faultstring, { retriesLeft = 3 } = {}) {
|
|
231
|
+
const error = new Error(`soap:Server: ${faultstring}`)
|
|
232
|
+
Object.assign(error, soapFault(faultstring))
|
|
233
|
+
return { error, attemptNumber: 1, retriesLeft, retriesConsumed: 0, retryDelay: 0 }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
it('aborts a blocked fault with a STRING message — the original incident', () => {
|
|
237
|
+
// Regression: this is the "Mandatory fields in context not set: [id]"
|
|
238
|
+
// fault that aborts retry. Before the fix, `new AbortError(context)`
|
|
239
|
+
// stored the context object as `.message`, so downstream
|
|
240
|
+
// `error.message.includes(...)` threw "includes is not a function".
|
|
241
|
+
let thrown
|
|
242
|
+
try {
|
|
243
|
+
handleFailedAttempt(attemptContext('Mandatory fields in context not set: [id]'), 'create')
|
|
244
|
+
} catch (e) {
|
|
245
|
+
thrown = e
|
|
246
|
+
}
|
|
247
|
+
expect(thrown).toBeInstanceOf(Error)
|
|
248
|
+
expect(typeof thrown.message).toBe('string')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('aborts with the inner Error so wrapSoapFault can still recover the fault', () => {
|
|
252
|
+
let thrown
|
|
253
|
+
try {
|
|
254
|
+
handleFailedAttempt(attemptContext('Mandatory fields in context not set: [id]'), 'create')
|
|
255
|
+
} catch (e) {
|
|
256
|
+
thrown = e
|
|
257
|
+
}
|
|
258
|
+
const recovered = wrapSoapFault(thrown)
|
|
259
|
+
expect(recovered.message).toContain('Mandatory fields in context not set: [id]')
|
|
260
|
+
expect(recovered.faultstring).toContain('Mandatory fields in context not set: [id]')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('does not throw for a transient (non-blocked) fault — lets p-retry continue', () => {
|
|
264
|
+
expect(() =>
|
|
265
|
+
handleFailedAttempt(attemptContext('Could not acquire claim for context 2572225'), 'create')
|
|
266
|
+
).not.toThrow()
|
|
267
|
+
})
|
|
268
|
+
})
|