Skip to content

Add shared HTTP retry wrapper and migrated wallet backend - #365

Merged
davedumto merged 1 commit into
Vellar-Wallet:devfrom
Y33t-dev:contrib/issue-301-http-retry-wrapper
Aug 31, 2026
Merged

Add shared HTTP retry wrapper and migrated wallet backend#365
davedumto merged 1 commit into
Vellar-Wallet:devfrom
Y33t-dev:contrib/issue-301-http-retry-wrapper

Conversation

@Y33t-dev

Copy link
Copy Markdown

closes #301

Summary

HTTP retry handling is duplicated across call sites instead of living centrally. src/http-backend.ts defines one private post() helper and then repeats the same request-and-check block three times; src/policy-client.ts repeats the shape again in its own req(), with its own error class. Neither retries at all, so a single dropped connection surfaces as a failed wallet creation.

This adds contrib/examples/issue-301-http-retry-wrapper: the shared wrapper that centralization needs, written against the real call sites so it can be dropped into createHttpWalletBackend without changing any public signature.

The part that actually matters: what must not be retried

Retrying HTTP is not looping until something works. Two of the three wallet endpoints are unsafe to retry blindly, and getting this wrong costs real money:

Endpoint Retryable Why
POST /wallet/connect yes A pure lookup. No side effect, so a replay costs at most a wasted request.
POST /wallet/create no Deploys a wallet. If the response is lost, it may already have deployed.
POST /wallet/submit no Submits a signed transaction. Retrying can double-submit and pay twice.

So the rule enforced is: retry only on positive evidence that the server reached no decision — a transport error, or 408/429/502/503/504.

Everything else is terminal, including 500. A 500 means the handler ran and failed partway, which is exactly the state where a retry can duplicate a write; a 503 means nothing was decided. That distinction is the whole safety argument, and it matches the reasoning already written down in src/policy-types.ts (isRetryableStatus) and src/x402-guards.ts (isRetryableSettleFailure), so the SDK keeps one consistent story about when a retry is safe.

Retrying is also opt-in (retryable defaults to false), so no call site inherits retries it never asked for. The safe default is the one that cannot double-spend.

Design notes

  • Exhausted retries return the last response rather than a wrapper error, so callers keep interpreting failures exactly as they do today. The wrapper never needs to know about WalletApiError or PolicyApiError, which is what lets one wrapper serve both files despite their different error types.
  • Backoff is exponential with subtractive jitter applied after the cap, so a delay stays within [exp * (1 - jitter), exp] and can never exceed maxDelayMs. Jitter matters when a gateway comes back up: without it, every client that failed at the same moment retries at the same moment.
  • sleep and random are injectable, so the tests assert exact delay sequences without waiting.
  • Abort is not transient: an aborted request stops retrying immediately rather than being treated as a transient fault.

retrying-wallet-backend.ts shows all three real call sites migrated, keeping the interface, error type and base-URL handling identical. The only change is that each call site declares its retryability and the retry lives in one place.

Tests

50 tests covering retryable and non-retryable statuses, transport errors, abort handling, exact backoff sequences, and the three migrated call sites — including the assertions that /wallet/submit and /wallet/create are attempted exactly once on a 503.

npx vitest run contrib/examples/issue-301-http-retry-wrapper

Notes for the maintainer

Scoped to contrib/ per CONTRIBUTING.md, so src/http-backend.ts is not modified here. The migrated backend is a working, tested reference for that change rather than a patch; happy to apply it to src/ directly if you widen the scope on the issue.

/wallet/create and /wallet/submit could become retryable with an idempotency key echoed by the gateway, but that is a protocol change on both sides, so their non-retryability is currently fixed rather than configurable.

The 18 test failures on dev (in contrib/examples/ and src/session.test.ts, which also fails tsc) are pre-existing and untouched by this branch.

Issue Vellar-Wallet#301: retry handling is duplicated across HTTP call sites instead of
living centrally. src/http-backend.ts repeats the same request-and-check
block three times, and src/policy-client.ts repeats the shape again with its
own error class. Neither retries, so a dropped connection surfaces as a
failed wallet creation.

Adds contrib/examples/issue-301-http-retry-wrapper with the wrapper that
centralization needs, written against the real call sites so it can be
dropped in without changing any public signature.

The substance is which requests must NOT be retried. /wallet/submit sends a
signed transaction and /wallet/create deploys a wallet; if either response
is lost, the work may already have happened, so retrying can double-submit.
Only failures proving the server reached no decision are retried: transport
errors and 408/429/502/503/504. A 500 is deliberately terminal, since the
handler ran and may have completed the side effect. This matches the
existing reasoning in policy-types.ts and x402-guards.ts.

Retrying is opt-in, so no call site inherits retries it did not request.
Backoff is exponential with subtractive jitter applied after the cap, so a
delay can never exceed maxDelayMs. sleep and random are injectable, letting
the tests assert exact delay sequences without waiting.

50 tests cover retryable and non-retryable statuses, transport errors,
abort handling, backoff sequences, and the three migrated call sites.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Y33t-dev 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

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Y33t-dev is attempting to deploy a commit to the david's projects Team on Vercel.

A member of the Team first needs to authorize it.

@davedumto
davedumto merged commit a01f855 into Vellar-Wallet:dev Aug 31, 2026
1 of 2 checks passed
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.

2 participants