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: