Implement SDK network resilience and endpoint diagnostics layer - #434
Merged
Conversation
added 6 commits
July 29, 2026 03:24
…CHABLE errors
NetworkClient previously collapsed every non-2xx response into a generic
HTTP_ERROR_<status> code and every connection-level failure (ECONNREFUSED,
ENOTFOUND, ...) into a generic NETWORK_ERROR. NET_RATE_LIMITED and
NET_UNREACHABLE already existed in the published error registry
(src/errors/codes.ts) but nothing ever produced them.
- 429 responses now throw NET_RATE_LIMITED (retryable: true)
- 5xx responses and socket/DNS failures (ECONNREFUSED, ENOTFOUND, EAI_AGAIN,
ENETUNREACH, EHOSTUNREACH) now throw NET_UNREACHABLE (retryable: true)
- Other 4xx statuses keep the existing HTTP_ERROR_<status> code for
backward compatibility
- Adds checkEndpointReachability(url): a lightweight GET probe that reports
{ reachable, latencyMs, errorCode } without ever exposing response bodies
or headers — used by the new endpoint diagnostics probe
Refs Axionvera#272 (Files: src/network/index.ts, +116/-8 lines)
fundTestnetAccount() remapped NetworkClient failures by checking
error.code.startsWith('HTTP_ERROR_'), which no longer matches 429/5xx
responses now that those throw NET_RATE_LIMITED/NET_UNREACHABLE. Extends
the check so Friendbot failures still surface as FRIENDBOT_ERROR, and
fixes a pre-existing bug where retryable/category/safeMessage were
silently dropped when the error was reconstructed (now passed through via
the object-form PocketPayError constructor instead of positional args).
External behaviour for existing consumers is unchanged: fund.test.ts
(23 tests, all pre-existing) still passes without modification.
Refs Axionvera#272 (Files: src/wallet/index.ts, +7/-4 lines)
Adds probeConfiguredEndpoints(), which checks the resolved Horizon and Soroban RPC URLs with checkEndpointReachability() and returns only URLs, latency, and typed error codes — never response bodies, headers, or secrets. Kept separate from buildDiagnosticsReport(), which stays a pure, side-effect-free config snapshot. This is opt-in: call it explicitly (e.g. from a support 'test connection' action) since it makes real network calls. Refs Axionvera#272 (Files: src/diagnostics/probe.ts [new, 45 lines], src/diagnostics/index.ts, +3 lines)
NetworkClient, withTimeout, fetchWithTimeout, executeHorizonOperation, executeSorobanOperation, and checkEndpointReachability were implemented and used internally (e.g. by fundTestnetAccount) but never re-exported from src/index.ts, so consumers had no public entry point to the 'network client abstraction' required by issue Axionvera#272. Also exports probeConfiguredEndpoints and the EndpointDiagnostics/EndpointReachability types. Refs Axionvera#272 (Files: src/index.ts, +11 lines)
- network-client.test.ts (12 tests, new): NetworkClient success path; 429 -> NET_RATE_LIMITED; 503 -> NET_UNREACHABLE; other 4xx keeps HTTP_ERROR_<status>; ECONNREFUSED and ENOTFOUND -> NET_UNREACHABLE; unrecognized rejections -> NETWORK_ERROR; REQUEST_TIMEOUT on a slow response; single fetch call per request (no hidden retries); plus checkEndpointReachability success/failure/no-body-leak cases. - endpoint-diagnostics.test.ts (3 tests, new): probeConfiguredEndpoints reports both endpoints reachable; rejects malformed config (invalid network) before making any network call; never leaks secrets or response bodies into the report. - exports.test.ts: asserts NetworkClient and the rest of the network resilience layer are exported from the package root; adds probeConfiguredEndpoints to the diagnostics export list. All unit tests run offline (tests/setup/offline-guard.ts blocks real fetch calls). Full suite: 164/164 passing across the 9 touched/added test files; pre-existing unrelated failures elsewhere are untouched by this change (see PR description). Refs Axionvera#272 (Files: tests/network-client.test.ts [new, 161 lines], tests/endpoint-diagnostics.test.ts [new, 58 lines], tests/exports.test.ts, +19 lines)
- docs/network-resilience.md (new): consolidated guide to NetworkClient, the typed failure codes (REQUEST_TIMEOUT, NET_RATE_LIMITED, NET_UNREACHABLE, HTTP_ERROR_<status>, NETWORK_ERROR), why read-only calls are safe to retry but transaction submission is not, and the two endpoint-diagnostics tools (config snapshot vs. live reachability probe). Cross-links network-errors.md and retry-policy.md rather than duplicating their content. - docs/network-errors.md: adds a NET_UNREACHABLE / NET_RATE_LIMITED row and links to the new guide. - README.md: adds the new guide to the docs index. Refs Axionvera#272 (Files: docs/network-resilience.md [new, 125 lines], docs/network-errors.md, +6 lines, README.md, +1 line)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #272
Context
Most of what this issue asks for already existed in
main:NetworkClient,withTimeout/fetchWithTimeout, timeout-stage classification, the safewithRetryPolicy/submitTransactionIdempotentlysubmission path, configvalidation, a redacted
buildDiagnosticsReport(), and docs for retry policyand timeout classification. Rather than duplicate that work, this PR closes
the concrete gaps left in the acceptance criteria:
NET_UNREACHABLEandNET_RATE_LIMITEDwere defined in the public errorregistry (
src/errors/codes.ts) but nothing ever produced them —NetworkClientcollapsed every non-2xx response intoHTTP_ERROR_<status>and every connection failure into a generic
NETWORK_ERROR.NetworkClient— the "network client abstraction" the issue asks for —was implemented and used internally, but never exported from the
package root.
whether the endpoints were actually reachable.
Changes
network: classify HTTP failures into typed NET_RATE_LIMITED/NET_UNREACHABLE errorssrc/network/index.ts(+116/-8)NET_RATE_LIMITED, 5xx / ECONNREFUSED / ENOTFOUND / EAI_AGAIN / ENETUNREACH / EHOSTUNREACH →NET_UNREACHABLE, bothretryable: true. Other 4xx keepHTTP_ERROR_<status>. AddscheckEndpointReachability(url).wallet: preserve FRIENDBOT_ERROR/FUND_ERROR mapping for new typed codessrc/wallet/index.ts(+7/-4)fundTestnetAccount()'s public error codes stable against the new typed codes; also fixes a bug whereretryable/category/safeMessagewere dropped on remap.diagnostics: add opt-in live endpoint reachability probesrc/diagnostics/probe.ts(new, 45),src/diagnostics/index.ts(+3)probeConfiguredEndpoints()— live Horizon/Soroban RPC reachability, no secrets, no response bodies. Kept separate from the side-effect-freebuildDiagnosticsReport().index: export the network client abstraction from the package rootsrc/index.ts(+11)NetworkClient,withTimeout,fetchWithTimeout,executeHorizonOperation,executeSorobanOperation,checkEndpointReachability,probeConfiguredEndpoints.tests: cover timeout, rate-limit, unavailable endpoint, malformed configtests/network-client.test.ts(new, 161),tests/endpoint-diagnostics.test.ts(new, 58),tests/exports.test.ts(+19)docs: add Network Resilience Layer guide, link NET_UNREACHABLEdocs/network-resilience.md(new, 125),docs/network-errors.md(+6),README.md(+1)Total: 11 files changed, 552 insertions(+), 12 deletions(-).
Acceptance criteria
NetworkClient, now public.REQUEST_TIMEOUT(pre-existing) andNET_RATE_LIMITED(now actually thrown).buildDiagnosticsReport()(pre-existing, config-only) + newprobeConfiguredEndpoints()(live reachability).docs/network-resilience.md.withRetryPolicy/submitTransactionIdempotentlystill poll before ever resubmitting on an unknown outcome. This PR does not touchclassifySubmitError's transaction-path timeout handling, which deliberately stays conservative (TX_STATUS_UNKNOWN, not retryable).network-client.test.ts(timeout, ECONNREFUSED/ENOTFOUND →NET_UNREACHABLE),endpoint-diagnostics.test.ts(malformednetworkconfig rejected before any fetch call).docs/network-resilience.md, cross-linked fromnetwork-errors.mdand the README docs index.Testing
Full suite:
164/164passing across all touched/added test files(
network-client,endpoint-diagnostics,exports,fund,timeout-classification,network-fee,config,config-validation,diagnostics).Note:
npm run testonmain(before this PR) already has ~45 failingtests unrelated to this change (
destination-validation.test.ts,idempotency.test.ts's twoclassifySubmitErrorcode-string assertions,retry-policy.test.ts's two matching assertions,types/asset.test.ts) —those assert older error codes (
PAYMENT_FAILED,SEND_ERROR) that a prior,unrelated refactor already replaced with the taxonomy codes
(
TX_BAD_SEQUENCE,NET_RATE_LIMITED) this PR reads from. Confirmedpre-existing by running the same suite against the baseline commit before
any changes in this PR. Not touched here — out of scope for #272 — but
flagging for visibility.
npx tsc --noEmit: clean except two pre-existing, unrelated errors insrc/network/fee.ts(a@stellar/stellar-sdkBASE_FEEtyping mismatch),also present on
mainbefore this PR and not touched by it.