From 213721c9d4c0fcbde317ff53d907af0b8cc9fff1 Mon Sep 17 00:00:00 2001 From: hermiondev Date: Mon, 28 Sep 2026 05:25:17 +0900 Subject: [PATCH] fix: bound unbounded fetch calls with 30s AbortSignal.timeout - alchemySendHttp.ts: JSON-RPC HTTP sender had no timeout -> hangs forever if upstream accepts TCP but never replies - sendRestPayload.ts: same pattern in REST payload sender (also amplifies: inside maxRetries loop, each stalled attempt is unbounded) Guarded feature-detect (AbortSignal.timeout) keeps fetch-ponyfill / older runtime compatibility. --- src/web3-adapter/alchemySendHttp.ts | 8 ++++++++ src/web3-adapter/sendRestPayload.ts | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/web3-adapter/alchemySendHttp.ts b/src/web3-adapter/alchemySendHttp.ts index 506cfad..fdb29f9 100644 --- a/src/web3-adapter/alchemySendHttp.ts +++ b/src/web3-adapter/alchemySendHttp.ts @@ -11,12 +11,20 @@ const ALCHEMY_HEADERS = new Headers({ }); const RATE_LIMIT_STATUS = 429; +// Requests without a bound can hang indefinitely when the upstream stalls +// (TCP accepted, no response). Default to 30s so callers surface a network +// error instead of an unresolved promise. +const DEFAULT_REQUEST_TIMEOUT = 30_000; + export function makeJsonRpcHttpSender(url: string): AlchemySendJsonRpcFunction { return async (request) => { const response = await fetch(url, { method: "POST", headers: ALCHEMY_HEADERS, body: JSON.stringify(request), + ...(typeof AbortSignal !== "undefined" && AbortSignal.timeout + ? { signal: AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT) } + : {}), }); const { status } = response; switch (status) { diff --git a/src/web3-adapter/sendRestPayload.ts b/src/web3-adapter/sendRestPayload.ts index 092cb2f..312e445 100644 --- a/src/web3-adapter/sendRestPayload.ts +++ b/src/web3-adapter/sendRestPayload.ts @@ -3,6 +3,10 @@ import URI from "urijs"; import { FullConfig } from "../types"; import { delay } from "../util/promises"; +// Unbounded fetches hang forever when an upstream stalls (TCP accepted, no +// response). Bound REST calls at 30s by default. +const DEFAULT_REQUEST_TIMEOUT = 30000; + export interface RestPayloadSender { sendRestPayload: SendRestPayloadFunction; } @@ -67,7 +71,11 @@ export function makeRestPayloadSender({ .path(isNftPath(path) ? formatNftPath(path) : apiKey + path) .toString(); for (let i = 0; i < maxRetries + 1; i++) { - const response = await fetch(endpoint); + const response = await fetch(endpoint, { + ...(typeof AbortSignal !== "undefined" && AbortSignal.timeout + ? { signal: AbortSignal.timeout(DEFAULT_REQUEST_TIMEOUT) } + : {}), + }); const { status } = response; switch (status) { case 200: