feat(stellar): add connect and request timeouts to the Horizon and RP… - #223
Merged
truthixify merged 1 commit intoSep 25, 2026
Merged
Conversation
…C clients createHorizonClient() and createRpcClient() now give every attempt a connect timeout (response headers, default 10 s) and a request timeout (whole attempt including the body, default 30 s), configurable per client and per call, with 0 to turn either off. A timed-out attempt is aborted through its own AbortController before the client retries or fails over, and the fetch and body read are raced against the deadline so a fetch that ignores the signal cannot hang a scan. Timeouts throw the new RPCTimeoutError (WRAITH/NETWORK/RPC_TIMEOUT) with the URL, endpoint, attempt number, phase and timeout; when retries run out, RPCRetryExhaustedError keeps the last attempt's error on cause. The RPC client now marks an endpoint healthy only after the body has been read, so an endpoint that sends headers and then stalls still trips the circuit breaker. Closes wraith-protocol#202
|
@aratass 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! 🚀 |
Contributor
|
Merged. Thanks @aratass. Timeout, abort, retry, and failover behavior are well covered. |
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 #202.
Summary
The Horizon and Soroban RPC clients retry failed requests, but an attempt that never resolved held the whole call, and any scan waiting on it, forever. This PR gives every attempt a deadline:
createHorizonClient()andcreateRpcClient()taketimeouts: { connectMs, requestMs }, and each call can override them (horizon.get(path, { timeouts }),rpc.request(method, path, body, { timeouts })).0turns a timeout off.AbortController. When a timeout fires, the request is aborted first; only then does the Horizon client retry, or the RPC client count the failure toward its circuit breaker and fail over.RPCTimeoutError(WRAITH/NETWORK/RPC_TIMEOUT, aWraithNetworkError) withurl,endpoint,attempt,phaseandtimeoutMs. When retries run out,RPCRetryExhaustedErrorkeeps the last attempt's error oncause, so the endpoint and attempt that timed out are not lost.Design
connectMsfetch()has no separate hook for the handshake, so this is the closest portable connect timeout.requestMssrc/chains/stellar/timeouts.tsholdsresolveTimeouts()andAttemptDeadline, which both clients use.resolveTimeouts()layers defaults, then client config, then the call's options. A negative,NaN, infinite or too-large value (oversetTimeout's 2 147 483 647 ms) throws aRangeErrorwhen the client is created or the call is made.AttemptDeadlineowns theAbortSignalpassed tofetch. When a timer fires, it rejects the attempt withRPCTimeoutErrorand aborts the signal.fetchthat ignores the signal (some polyfills do) cannot hang the attempt. If the abort surfaces as anAbortError, the attempt reports the timeout instead.finally, so none outlives a request. The tests assertvi.getTimerCount() === 0.AbortControllerdoes not exist, the race still enforces the timeout.One behaviour fix in
createRpcClient().markHealthy()as soon as the headers of a 200 arrived, before reading the body. An endpoint that sent headers and then stalled reset its failure count on every attempt. It never tripped the circuit breaker, so request timeouts on such an endpoint could never cause a failover.Defaults. Both clients now time out by default instead of waiting forever.
timeouts: { connectMs: 0, requestMs: 0 }restores the old behaviour.POST /transactionsresponse until the transaction is in a ledger, so the docs show a longer per-call timeout for submissions.Error shape
causestays out of serialised output. It is non-enumerable and left out oftoJSON(), the same as on the wallet errors from [Wave 9] Add wallet adapter failure and disconnect conformance tests #214.RPCRetryExhaustedError's constructor gains an optional fourthoptionsargument.endpointFailoverreason readsTimeout on <endpoint>: connect timeout of 5000ms.Tests
test/chains/stellar/request-timeouts.test.ts(24 tests) uses a scripted fakefetchwith fake timers.What the tests cover:
connectMs(not atconnectMs - 1) and retried;endpointFailoverfires;RPCRetryExhaustedErrorwhosecausenames the last endpoint and attempt 4;requestMsand still fails over;0disables a timeout;signalbeing passed tofetch.resolveTimeouts()defaults, layering and validation;AttemptDeadlinereports the timeout rather than anAbortError.test/errors.test.ts:RPCTimeoutError's place in the hierarchy, its code, fields, context,toJSON()anddescribe();RPCRetryExhaustedErrorkeepscausenon-enumerable and out of JSON.I also broke the implementation on purpose to check that the tests catch real regressions. Each change below makes at least one test fail:
fetchwithout racing the deadlinedispose()in the RPC clientdispose()in the Horizon clientAbortErrorinstead of the timeoutDocs
docs/chains/stellar-request-timeouts.mdcovers the options, what happens on a timeout and the error fields, with examples.docs/errors.mdaddsRPCTimeoutErrorto the hierarchy and the network table, and explainscauseonRPCRetryExhaustedError.CHANGELOG.mdhas entries under Added and Changed;MIGRATING.mdhas a new section.etc/sdk.api.mdandetc/sdk-stellar.api.mdare regenerated.Verification
Based on
develop@d7cfec6, with no new dependencies. Run locally with pnpm 10 on Node 22, in CI's order.develop(before)pnpm run format:checkpnpm buildpnpm api:checkpnpm testpnpm test:exportsThe 27 extra tests are 24 in
request-timeouts.test.tsand 3 inerrors.test.ts.Bundle size.
pnpm sizeexits 0 on both.🤖 Generated with Claude Code
https://claude.ai/code/session_01UhuzUyVKtcr7g1gbqgu3Mg