From 1ee36dca366007467368b3f89843e3584918fee0 Mon Sep 17 00:00:00 2001 From: degenpepe Date: Wed, 18 Feb 2026 12:21:24 +0800 Subject: [PATCH] fix: add retry with exponential backoff for server/network errors API calls to the ACP backend can fail due to transient 5xx errors or network timeouts. Currently these failures crash the caller immediately. Adds automatic retry (up to 3 attempts) with exponential backoff (1s, 2s, 4s) for: - HTTP 5xx server errors - Network errors (ECONNRESET, ETIMEDOUT, etc.) Client errors (4xx) are NOT retried as they indicate permanent failures (auth, validation, not found). This improves reliability for long-running seller and buyer agents during API instability. --- src/acpClient.ts | 72 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/acpClient.ts b/src/acpClient.ts index 90f5802..66d97b4 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -307,30 +307,66 @@ class AcpClient { data?: Record, errCallback?: (err: AxiosError) => void, ): Promise["data"] | undefined> { - try { - const response = await this.acpClient.request>({ - url, - method, - params, - data, - }); + const maxRetries = 3; + let lastError: unknown; - return response.data.data; - } catch (err) { - if (err instanceof AxiosError) { - if (errCallback) { - errCallback(err); - } else if (err.response?.data.error?.message) { - throw new AcpError(err.response?.data.error.message as string); - } else { - throw new AcpError(`Failed to fetch ${url}: ${err.message}`, err); + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const response = await this.acpClient.request>({ + url, + method, + params, + data, + }); + + return response.data.data; + } catch (err) { + lastError = err; + + if (err instanceof AxiosError) { + // Don't retry on client errors (4xx) — only on network/server errors + if (err.response && err.response.status < 500) { + if (errCallback) { + errCallback(err); + return undefined; + } else if (err.response?.data.error?.message) { + throw new AcpError(err.response?.data.error.message as string); + } else { + throw new AcpError( + `Failed to fetch ${url}: ${err.message}`, + err, + ); + } + } + + // Retry on 5xx or network errors with exponential backoff + if (attempt < maxRetries - 1) { + const delay = Math.min(1000 * Math.pow(2, attempt), 5000); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } } + + break; + } + } + + if (lastError instanceof AxiosError) { + if (errCallback) { + errCallback(lastError); + } else if (lastError.response?.data.error?.message) { + throw new AcpError(lastError.response?.data.error.message as string); } else { throw new AcpError( - `Failed to fetch ACP Endpoint: ${url} (network error)`, - err, + `Failed to fetch ${url}: ${lastError.message}`, + lastError, ); } + } else { + throw new AcpError( + `Failed to fetch ACP Endpoint: ${url} (network error)`, + lastError, + ); } }