|
| 1 | +# Network Resilience Layer |
| 2 | + |
| 3 | +This is the top-level guide to how the SDK talks to the network: the client |
| 4 | +abstraction, the typed errors it produces, how read-only calls differ from |
| 5 | +state-changing ones, and how to check endpoint health. For the full status |
| 6 | +code and result-code reference, see [Network Error Handling](./network-errors.md); |
| 7 | +for the transaction retry state machine, see [Safe Retry Policy](./retry-policy.md). |
| 8 | + |
| 9 | +## `NetworkClient` |
| 10 | + |
| 11 | +All raw `fetch` calls in the SDK are expected to go through `NetworkClient` (or |
| 12 | +`fetchWithTimeout`, which it's built on) rather than being made ad hoc from |
| 13 | +individual modules. It centralises three things every call needs: a timeout |
| 14 | +budget, JSON parsing, and typed error classification. |
| 15 | + |
| 16 | +```ts |
| 17 | +import { NetworkClient } from 'stellar-pocketpay-sdk'; |
| 18 | + |
| 19 | +const client = new NetworkClient({ |
| 20 | + baseUrl: 'https://friendbot.stellar.org', |
| 21 | + defaultTimeoutMs: 10_000, |
| 22 | +}); |
| 23 | + |
| 24 | +const data = await client.get('?addr=GABC...', { operation: 'Friendbot funding request' }); |
| 25 | +``` |
| 26 | + |
| 27 | +`get()` and `post()` both accept a per-call `timeoutMs` and `operation` label |
| 28 | +(used in error messages and timeout-stage inference — see |
| 29 | +[Timeout Classification](./timeout-classification.md)). |
| 30 | + |
| 31 | +## Typed failure codes |
| 32 | + |
| 33 | +`NetworkClient` classifies every failure into one of these codes before |
| 34 | +throwing, so callers can branch on `error.code` / `error.retryable` instead of |
| 35 | +inspecting HTTP status numbers or `error.message`: |
| 36 | + |
| 37 | +| Code | When | Retryable | |
| 38 | +|---|---|---| |
| 39 | +| `REQUEST_TIMEOUT` | The SDK's own timeout budget elapsed (see `withTimeout`) | Yes (unless the stage is `submission`/`confirmation` — see below) | |
| 40 | +| `NET_RATE_LIMITED` | HTTP 429 | Yes | |
| 41 | +| `NET_UNREACHABLE` | HTTP 5xx, or the request never reached the endpoint (`ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`, `ENETUNREACH`, `EHOSTUNREACH`) | Yes | |
| 42 | +| `HTTP_ERROR_<status>` | Any other non-2xx response (400, 401, 403, 404, …) | No | |
| 43 | +| `NETWORK_ERROR` | An unrecognized fetch rejection | Depends — inspect `error.cause` | |
| 44 | + |
| 45 | +All of the above are published, stable codes in `ERROR_CODES` (see |
| 46 | +`src/errors/codes.ts`); use `describeError(code)` to get a safe user-facing |
| 47 | +message and developer hint for any of them. |
| 48 | + |
| 49 | +```ts |
| 50 | +import { NetworkClient, describeError } from 'stellar-pocketpay-sdk'; |
| 51 | + |
| 52 | +try { |
| 53 | + await client.get('/accounts/GABC...'); |
| 54 | +} catch (error) { |
| 55 | + if (error instanceof PocketPayError) { |
| 56 | + const { safeMessage, retryable } = describeError(error.code); |
| 57 | + if (retryable) { |
| 58 | + // back off and retry the same read-only request |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +## Read-only calls vs. state-changing submission |
| 65 | + |
| 66 | +**Read-only requests** (account lookups, fee stats, transaction history, |
| 67 | +Friendbot funding) are safe to retry whenever `error.retryable` is `true`. |
| 68 | +Nothing on the server changes state just because a GET was retried. |
| 69 | + |
| 70 | +**Transaction submission is different.** A submission that times out or hits |
| 71 | +`NET_UNREACHABLE` may or may not have reached the network — resubmitting |
| 72 | +blindly risks a duplicate payment. The SDK never does this automatically: |
| 73 | + |
| 74 | +- `submitTransactionIdempotently()` resolves this by polling Horizon for the |
| 75 | + transaction hash before deciding anything. |
| 76 | +- `withRetryPolicy()` builds on that: it only resubmits the same envelope for |
| 77 | + `retryable_failure` outcomes, and always requires a status check |
| 78 | + (`requiresStatusCheck`) before any further action on `unknown_status`. |
| 79 | + |
| 80 | +See [Safe Retry Policy](./retry-policy.md) for the full state machine. The |
| 81 | +short version: **retry reads freely; never retry a submission without going |
| 82 | +through `submitTransactionIdempotently` or `withRetryPolicy`.** |
| 83 | + |
| 84 | +## Endpoint diagnostics |
| 85 | + |
| 86 | +Two complementary tools are available, both safe to share with support — |
| 87 | +neither ever includes secret keys, signed XDR, or response bodies. |
| 88 | + |
| 89 | +### Config snapshot (no network calls) |
| 90 | + |
| 91 | +`buildDiagnosticsReport()` returns a redacted snapshot of the resolved |
| 92 | +configuration — network, Horizon/Soroban URLs, timeout, capability status — |
| 93 | +without making any request: |
| 94 | + |
| 95 | +```ts |
| 96 | +import { buildDiagnosticsReport } from 'stellar-pocketpay-sdk'; |
| 97 | + |
| 98 | +const report = buildDiagnosticsReport({ network: 'testnet' }); |
| 99 | +``` |
| 100 | + |
| 101 | +### Live reachability probe (opt-in network calls) |
| 102 | + |
| 103 | +`checkEndpointReachability(url)` and `probeConfiguredEndpoints(config)` make a |
| 104 | +lightweight GET against an endpoint and report only whether it responded, how |
| 105 | +long it took, and a typed error code if it didn't — never the response body: |
| 106 | + |
| 107 | +```ts |
| 108 | +import { probeConfiguredEndpoints } from 'stellar-pocketpay-sdk'; |
| 109 | + |
| 110 | +const diagnostics = await probeConfiguredEndpoints({ network: 'testnet' }); |
| 111 | +// { generatedAt, horizon: { url, reachable, latencyMs }, sorobanRpc: { ... } } |
| 112 | +``` |
| 113 | + |
| 114 | +Because this makes real network calls, it is never invoked automatically by |
| 115 | +`buildDiagnosticsReport()` — call it explicitly when you need to check live |
| 116 | +connectivity (e.g. a support "test connection" button). |
| 117 | + |
| 118 | +## Malformed configuration |
| 119 | + |
| 120 | +Every network call resolves configuration through `resolveConfig()` / |
| 121 | +`validatePocketPayConfig()` first, which validates the network name, Horizon |
| 122 | +and Soroban RPC URLs, timeout, and contract ID before any request is made. An |
| 123 | +invalid config throws (or, for `validatePocketPayConfig`, reports structured |
| 124 | +issues) instead of silently falling back to a default — see |
| 125 | +[SDK Configuration](./configuration.md) for the full validation rules. |
0 commit comments