Skip to content

fix(sdk): Add comprehensive retry logic with exponential backoff for network failures - #256

Open
Vincent6581 wants to merge 2 commits into
Kevin737866:mainfrom
Vincent6581:fix/issue-191-retry-logic
Open

fix(sdk): Add comprehensive retry logic with exponential backoff for network failures#256
Vincent6581 wants to merge 2 commits into
Kevin737866:mainfrom
Vincent6581:fix/issue-191-retry-logic

Conversation

@Vincent6581

Copy link
Copy Markdown
Contributor

Summary

Adds an intelligent retry layer for transient network failures, per issue #191. Deterministic failures are never retried.

Acceptance criteria

Criterion Status
RetryConfig: maxRetries, baseDelayMs, maxDelayMs, backoffMultiplier ✅ + optional jitter; DEFAULT_RETRY_CONFIG in constants.ts
Retryable: network timeout, 429, Horizon 504, mempool full ✅ socket codes (ETIMEDOUT, ECONNRESET, …), HTTP 408/425/429/500/502/503/504, mempool is full / rate-limit message patterns, retryable Stellar result codes (tx_too_late, tx_bad_seq, …)
Non-retryable: invalid signature, insufficient funds, contract error ✅ also UNAUTHORIZED, INVALID_PARAMETERS, KYC/compliance, tx_bad_auth, op_*, any 4xx
Retry state events for UI feedback RetryEvent (retry / exhausted / succeeded) via onRetryEvent, or sdk.onRetry(listener)

Public API

import { withRetry, isRetryableError, classifyError, RateLimitError } from 'stellar-rwa-sdk';

const result = await withRetry(
  (attempt) => horizon.submitTransaction(tx),
  {
    config: { maxRetries: 5, baseDelayMs: 250, maxDelayMs: 20_000, backoffMultiplier: 2 },
    onRetryEvent: (e) => {
      if (e.type === 'retry') console.warn(`attempt ${e.attempt}/${e.maxAttempts} failed, retrying in ${e.delayMs}ms`);
    },
  }
);

Or via the SDK instance:

const sdk = new StellarRWASDK({ ...config, retry: { maxRetries: 5 } });
sdk.onRetry((e) => ui.showBackoff(e));
await sdk.withRetry((attempt) => sdk.marketClient.placeOrder(opts));

Design notes

  • classifyError(error) returns { retryable, reason } — the reason string is handy for logs/telemetry. isRetryableError() wraps it.
  • Backoff: min(maxDelayMs, baseDelayMs * multiplier^index) then ± jitter. sleep and rng are injectable so backoff timing is deterministic under test; a RateLimitError.retryAfterMs raises the floor of the next delay.
  • Optional { aborted: boolean } signal stops further retries between attempts.

Incidental fix

errors.ts had a syntax error in the SUGGESTED_ACTIONS map ([ErrorCode.TX_BAD_SEQ' — stray quote) that prevented the module from compiling at all; that also masked a missing ParsedHorizonError type import. Both are fixed here since the file is being modified.

Files changed

  • sdk/src/errors.ts — retry classification, backoff, withRetry, new error classes
  • sdk/src/constants.ts — retry config + retryable code/pattern tables
  • sdk/src/index.ts — exports; StellarRWASDK.withRetry() / .onRetry() / config.retry

Closes #191

Adds a transient-failure retry layer:

- RetryConfig (maxRetries, baseDelayMs, maxDelayMs, backoffMultiplier,
  jitter) with DEFAULT_RETRY_CONFIG
- classifyError() / isRetryableError() distinguishing transient failures
  (network timeouts, socket errors, HTTP 408/425/429/5xx, Horizon 504,
  mempool full, retryable Stellar result codes) from deterministic ones
  (invalid signature, insufficient funds, contract errors, unauthorized,
  invalid parameters, KYC/compliance failures)
- computeRetryDelay() — capped exponential backoff with bounded jitter
- withRetry(fn, options) — honours Retry-After on RateLimitError, an
  optional abort signal, and injectable sleep/rng for testing
- RetryEvent stream ('retry' | 'exhausted' | 'succeeded') via
  onRetryEvent for UI feedback
- New RateLimitError / MempoolFullError classes
- StellarRWASDK.withRetry() + StellarRWASDK.onRetry() using a per-SDK
  retryConfig (configurable via new config.retry)
- Constants: RETRYABLE_HTTP_STATUS_CODES, RETRYABLE_NETWORK_ERROR_CODES,
  RETRYABLE_STELLAR_RESULT_CODES, RETRYABLE_ERROR_MESSAGE_PATTERNS

Also repairs a pre-existing syntax error in the SUGGESTED_ACTIONS map
(TX_BAD_SEQ key) and a missing ParsedHorizonError type import that the
syntax error had been masking.

Files:
- sdk/src/errors.ts
- sdk/src/constants.ts
- sdk/src/index.ts

Closes Kevin737866#191
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@Vincent6581 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Kevin737866

Copy link
Copy Markdown
Owner

@Vincent6581 cconflict

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sdk): Add comprehensive retry logic with exponential backoff for network failures

2 participants