@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 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.5...main
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-xchange/soap-client",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "SOAP client for OX App Suite",
5
5
  "main": "index.js",
6
6
  "type": "module",
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: async error => {
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: async error => {
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
+ })