diff --git a/package.json b/package.json index 35762d5b6..0e7d578f3 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "graphql-ws": "^5.13.1", "gt3-server-node-express-sdk": "https://github.com/GaloyMoney/gt3-server-node-express-bypass#master", "i18n": "^0.15.1", - "ibex-client": "^3.1.0", + "ibex-client": "^3.2.0", "invoices": "^3.0.0", "ioredis": "^5.3.2", "ioredis-cache": "^2.0.0", diff --git a/src/app/cash-wallet-cutover/runtime-services.ts b/src/app/cash-wallet-cutover/runtime-services.ts index ce49a7028..be1e3cd59 100644 --- a/src/app/cash-wallet-cutover/runtime-services.ts +++ b/src/app/cash-wallet-cutover/runtime-services.ts @@ -126,8 +126,14 @@ const sleep: SleepFn = (delayMs: number) => const errorMessage = (error: Error): string => error.message || String(error) -const isIbexRateLimitError = (error: Error): boolean => - errorMessage(error).toLowerCase().includes("too many requests") +const isIbexRateLimitError = (error: Error): boolean => { + // Structural first (ENG-485): IbexError.httpCode (via ibex-client >= 3.2.0 + // ApiError) for returned errors, .status for raw thrown FetchErrors. Text + // match kept as a fallback for anything that carries neither. + const candidate = error as { httpCode?: unknown; status?: unknown } + if (candidate.httpCode === 429 || candidate.status === 429) return true + return errorMessage(error).toLowerCase().includes("too many requests") +} const withIbexRateLimitRetry = async ({ operation, diff --git a/src/services/ibex/errors.ts b/src/services/ibex/errors.ts index f038a8e07..c34d6c9be 100644 --- a/src/services/ibex/errors.ts +++ b/src/services/ibex/errors.ts @@ -15,7 +15,7 @@ export class IbexError extends DomainError { super(err) this.type = err.name this.level = level - // this.httpCode = err instanceof ApiError ? err.code : undefined + this.httpCode = err instanceof ApiError ? err.httpCode : undefined } } diff --git a/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts b/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts index 3dc469e49..4de21b46b 100644 --- a/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts +++ b/test/flash/unit/app/cash-wallet-cutover/runtime-services.spec.ts @@ -31,6 +31,25 @@ describe("cutover runtime services — IBEX rate-limit retry (ENG-483)", () => { expect(sleeps).toEqual([7, 7]) }) + it("retries on structural 429 (httpCode/status) with no rate-limit text (ENG-485)", async () => { + const structural = Object.assign(new Error("Request failed"), { status: 429 }) + const getRawAccountDetails = jest + .fn() + .mockRejectedValueOnce(structural) + .mockResolvedValue({ balance: 1 }) + + const services = createCashWalletMigrationRuntimeServices({ + getRawAccountDetails, + rateLimitRetryDelayMs: 1, + sleep: async () => undefined, + }) + + const result = await services.balanceReader.readSourceBalanceUsdCents(migration) + + expect(result).toBe("100") + expect(getRawAccountDetails).toHaveBeenCalledTimes(2) + }) + it("retries balance reads when the read RETURNS a rate-limit error", async () => { const getRawAccountDetails = jest .fn() diff --git a/yarn.lock b/yarn.lock index 398a36097..321f012b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8867,10 +8867,10 @@ i18n@^0.15.1: math-interval-parser "^2.0.1" mustache "^4.2.0" -ibex-client@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/ibex-client/-/ibex-client-3.1.1.tgz#8fe11b40f2b51d03fb6f5f613f558a06097d525b" - integrity sha512-9L2aYU+40GU1u0fQ1Ag0C1f3ncfJL0+d7YzkBEDSw1uqfaILc+Zeg23B9U3jssurJZJ+pJN2QFCWN0Ww0bsJZg== +ibex-client@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ibex-client/-/ibex-client-3.2.0.tgz#2a9a707e1c250c1bd226e7be99c5d5baa2f24ca2" + integrity sha512-jteyXwSBUIwEyjEXFXdJMUYtBdoZ2HloDoFuBm20rThN6jlR/2XAaimlitZjmImKE1u1wCLrAcaAXWH8AcOStA== dependencies: api "^6.1.2" node-cache "^5.1.2"