This page describes how the SDKs report errors and how to page through list results. It also covers the HTTP transport settings: retries, timeouts, TLS, proxies, a custom HTTP client, and logging.
The SDK examples assume a configured client. To set one up, see Integrate via SDK.
Every error that the client raises derives from CustodyError, so one catch (CustodyError) or except CustodyError handles any SDK failure. Each SDK represents HTTP errors differently.
CustodyError statusCode: HTTP status, or 0 if no response was received
├── ApiError status, reason, body, data (parsed JSON body)
│ ├── BadRequestError 400
│ ├── UnauthorizedError 401
│ ├── ForbiddenError 403
│ ├── NotFoundError 404
│ ├── ConflictError 409
│ ├── UnprocessableEntityError 422
│ └── ServiceError 5xx
├── NetworkError DNS, connection, TLS, or timeout failure; cause holds the original error
├── ConfigurationError
├── MissingConfigurationError
├── IntentTimeoutError lastState
├── ResponseSignatureError
└── AddressIntegrityErrorThe SDK raises other 4xx statuses as ApiError. It doesn't throw CustodyWarning. Instead, it passes the warning message to the logger.
Branch on the status code or the error class, never on the message text.
A failed intent isn't an HTTP error. Submitting an intent returns 202 Accepted, and the platform processes it afterward. If processing fails, waitAccepted throws a plain CustodyError with status code 0. See Work with intents.
The following examples handle 404 and 409 responses when you read a user:
import { ConflictError, NotFoundError } from '@ripple/ripple-custody-client-lib-typescript';
try {
const user = await client.governance.users.get(userId);
} catch (err) {
if (err instanceof NotFoundError) {
// The user doesn't exist, or isn't visible to you
} else if (err instanceof ConflictError) {
// For example, a duplicate ID or a revision mismatch
} else {
throw err;
}
}List methods return one page by default. A page has items and a nextStartingAfter cursor. To get the next page, pass the cursor as startingAfter. When the cursor is empty, there are no more pages.
Each SDK also has a way to read all pages:
- TypeScript: pass
autoPaginate: true. The method returns an array of every item instead of a page. It stops withCustodyErrorafter 1,000 pages. - Java: call
stream(...)(orstreamOrders,streamTransactions, and similar). The stream fetches pages lazily as you consume it. For list methods without a stream variant, build one withAutoPaginate.stream(...). - Python: pass
auto_paginate=True. The method returns a list of every item instead of a page.
In TypeScript and Python, automatic pagination loads every item into memory before it returns. For large result sets, page manually.
The following examples show both approaches:
import type {
CoreTrustedDomain,
CoreTrustedDomainsCollection,
} from '@ripple/ripple-custody-client-lib-typescript';
// One page at a time
let cursor: string | undefined;
do {
const page = (await client.governance.domains.list({
limit: 50,
startingAfter: cursor,
})) as CoreTrustedDomainsCollection;
for (const domain of page.items) console.log(domain.data.id);
cursor = page.nextStartingAfter || undefined;
} while (cursor);
// Every page at once
const all = (await client.governance.domains.list({
autoPaginate: true,
})) as CoreTrustedDomain[];For a complete program, see example 01 (listing and pagination). To get the examples, see Examples.
Each SDK lets you supply your own HTTP transport. Use this to add retries, timeouts, a proxy, tracing headers, or request logging. When you supply a transport, it owns the connection. The SDK's TLS settings (RC_VERIFY_SSL, RC_TLS_MIN_VERSION, RC_SSL_CA_CERT, RC_CA_CERT_DATA, RC_CERT_FILE, and RC_KEY_FILE) no longer apply, so configure TLS on your transport. Response-signature verification still applies.
Pass a fetchApi function to generateNewConfiguration and use the result as the client's transport:
import {
CustodyClient,
generateNewConfiguration,
loadConfiguration,
} from '@ripple/ripple-custody-client-lib-typescript';
const config = loadConfiguration();
const myFetch: typeof fetch = async (url, init) => fetch(url, init);
const sdkConfig = generateNewConfiguration({
apiBaseUrl: config.apiBaseUrl,
oidcIssuerUrl: config.oidcIssuerUrl,
tokenEndpoint: config.tokenEndpoint,
readPublicKey: config.readPublicKey,
signString: config.signString,
verifyResponses: config.verifyResponses,
notaryApiPublicKey: config.notaryApiPublicKey,
fetchApi: myFetch,
});
const client = new CustodyClient({ ...config, _sdkConfig: sdkConfig });generateNewConfiguration doesn't read config.toml, so pass it the values that loadConfiguration() returns, as shown. loadConfiguration() doesn't return the client ID or client secret. For the client-credentials grant, also pass oAuthServerClientId and oAuthServerClientSecret.
OAuth token requests use Node's global fetch, not fetchApi. A custom transport, including any proxy you configure on it, doesn't apply to them.
The SDK or its HTTP transport can retry a request. The defaults differ by language:
| SDK | Default behavior |
|---|---|
| TypeScript | Doesn't retry. A 401 response throws UnauthorizedError. |
| Java | Uses OkHttp's automatic retries after connection failures. |
| Python (synchronous client) | Uses urllib3's default retry policy. |
The language tabs below explain how to configure retries. The Python settings apply to the synchronous CustodyClient and urllib3, not an injected asynchronous transport. OAuth token requests can use a different transport, as described in Inject a custom HTTP client.
If you add retries for temporary failures, restrict them to GET requests. Account for any retries that the HTTP transport already makes. Don't automatically retry POST requests that submit, approve, or reject intents. Ripple Custody might already have accepted a request that timed out. Before you resubmit after an unclear failure, list recent intents and check whether the first one arrived.
The following examples retry only GET requests that fail with a transient status:
The SDK doesn't retry requests, including after a 401 response. To retry, add retries through a custom fetchApi. The example below adds retries for 429 and 5xx responses on GET requests.
const retryingFetch: typeof fetch = async (url, init) => {
const method = init?.method ?? 'GET';
for (let attempt = 1; ; attempt++) {
const response = await fetch(url, init);
const transient = response.status === 429 || response.status >= 500;
if (method !== 'GET' || !transient || attempt === 3) return response;
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
}
};Pass retryingFetch as fetchApi, as shown in Inject a custom HTTP client.
HTTP timeouts limit a single request. They're separate from the timeouts you pass to waitAccepted and the other intent wait methods, which limit how long the SDK keeps polling. See Work with intents.
TypeScript: the built-in transport sets no request timeout of its own. Token requests time out after 30 seconds. A request that times out throws
NetworkError. To limit API requests, use a customfetchApithat sets an abort signal:const fetchWithTimeout: typeof fetch = (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(30_000) });Java: the default client uses the OkHttp defaults of 10 seconds for connect, read, and write. To change them, set them on a custom
OkHttpClient, as shown in Inject a custom HTTP client.Python: set
config.timeoutto a number of seconds before you create the client. It applies to API requests and token requests, even when you inject a custom pool. A timeout set only on the pool doesn't protect API requests because the SDK overrides it.
The SDKs verify the server certificate by default. The TypeScript SDK also rejects an http:// API base URL unless the host is local. You configure the following settings in config.toml or with RC_* environment variables. For the key names and formats, see Configure the SDK.
| Setting | Environment variable | Notes |
|---|---|---|
| Minimum TLS version | RC_TLS_MIN_VERSION | TypeScript and Python. Default TLSv1_2. Set TLSv1_3 to require TLS 1.3. The Java SDK always accepts TLS 1.2 and 1.3 and has no setting. |
| Custom CA bundle | RC_SSL_CA_CERT (file path) or RC_CA_CERT_DATA (inline PEM) | Use when a private certificate authority (CA) issues your deployment's certificate. |
| Mutual TLS | RC_CERT_FILE and RC_KEY_FILE | Set both or neither. |
| Certificate verification | RC_VERIFY_SSL | Default true. Set false only for local development. |
These settings apply only to the transport the SDK builds. If you inject your own HTTP client, configure TLS on it.
How you route requests through a proxy differs by SDK.
The SDK has no proxy setting. Install undici in your project, and route API requests through a proxy with a custom fetchApi that uses undici's ProxyAgent:
import { ProxyAgent, fetch as undiciFetch } from 'undici';
const proxy = new ProxyAgent('http://proxy.example.com:3128');
const proxiedFetch: typeof fetch = (input, init) =>
undiciFetch(input as Parameters<typeof undiciFetch>[0], {
...(init as Parameters<typeof undiciFetch>[1]),
dispatcher: proxy,
}) as unknown as Promise<Response>;Pass proxiedFetch as fetchApi. Token requests don't go through fetchApi, so this doesn't proxy them.
Each SDK handles logging differently.
The SDK logs nothing by default. Set RC_LOG_LEVEL_NPM (for example, warn or debug) to log to the console, or route SDK logs to your own logger with setLogger:
import { setLogger } from '@ripple/ripple-custody-client-lib-typescript';
setLogger({
debug: () => {},
info: (msg) => console.info(msg),
warn: (msg) => console.warn(msg),
error: (msg, err) => console.error(msg, err),
});Call setLogger before you create the client.
- Configure the SDK: configuration keys and environment variables, including the TLS settings.
- Work with intents: submit intents and wait for their outcome with
PendingIntent. - Verify response signatures: pin the notary API public key and handle
ResponseSignatureError.