diff --git a/sdk/src/lib/api/HttpApi.ts b/sdk/src/lib/api/HttpApi.ts index d1759d6d2..eb9be45ca 100644 --- a/sdk/src/lib/api/HttpApi.ts +++ b/sdk/src/lib/api/HttpApi.ts @@ -1,11 +1,48 @@ +import { backoffRetrier, RetryOptions } from "../utils" + /** * Represents an abstract HTTP API. */ export default abstract class HttpApi { #apiUrl: string - constructor(apiUrl: string) { + /** + * Retry options for API requests. + */ + #retryOptions: RetryOptions + + constructor( + apiUrl: string, + retryOptions: RetryOptions = { + retries: 5, + backoffStepMs: 1000, + }, + ) { this.#apiUrl = apiUrl + this.#retryOptions = retryOptions + } + + /** + * Makes an HTTP request with retry logic. + * @param requestFn Function that returns a Promise of the HTTP response. + * @returns The HTTP response. + * @throws Error if the request fails after all retries. + */ + async #requestWithRetry( + requestFn: () => Promise, + ): Promise { + return backoffRetrier( + this.#retryOptions.retries, + this.#retryOptions.backoffStepMs, + )(async () => { + const response = await requestFn() + + if (!response.ok) { + throw new Error(`Request failed: ${await response.text()}`) + } + + return response + }) } /** @@ -18,10 +55,12 @@ export default abstract class HttpApi { endpoint: string, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - credentials: "include", - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + credentials: "include", + ...requestInit, + }), + ) } /** @@ -36,12 +75,14 @@ export default abstract class HttpApi { body: unknown, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - method: "POST", - body: JSON.stringify(body), - credentials: "include", - headers: { "Content-Type": "application/json" }, - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + method: "POST", + body: JSON.stringify(body), + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...requestInit, + }), + ) } } diff --git a/sdk/src/lib/api/TbtcApi.ts b/sdk/src/lib/api/TbtcApi.ts index 931e36601..c4ce503c4 100644 --- a/sdk/src/lib/api/TbtcApi.ts +++ b/sdk/src/lib/api/TbtcApi.ts @@ -38,12 +38,11 @@ export default class TbtcApi extends HttpApi { * otherwise. */ async saveReveal(revealData: SaveRevealRequest): Promise { - const response = await this.postRequest("reveals", revealData) - - if (!response.ok) - throw new Error( - `Reveal not saved properly in the database, response: ${response.status}`, - ) + const response = await this.postRequest("reveals", revealData).catch( + (error) => { + throw new Error(`Failed to save reveal: ${error}`) + }, + ) const { success } = (await response.json()) as { success: boolean } @@ -60,11 +59,11 @@ export default class TbtcApi extends HttpApi { depositStatus: DepositStatus fundingOutpoint: BitcoinTxOutpoint }> { - const response = await this.postRequest("deposits", depositData) - if (!response.ok) - throw new Error( - `Bitcoin deposit creation failed, response: ${response.status}`, - ) + const response = await this.postRequest("deposits", depositData).catch( + (error) => { + throw new Error(`Failed to create deposit: ${error}`) + }, + ) const responseData = (await response.json()) as CreateDepositResponse @@ -85,10 +84,9 @@ export default class TbtcApi extends HttpApi { async getDepositsByOwner(depositOwner: ChainIdentifier): Promise { const response = await this.getRequest( `deposits/${depositOwner.identifierHex}`, - ) - - if (!response.ok) - throw new Error(`Failed to fetch deposits: ${response.status}`) + ).catch((error) => { + throw new Error(`Failed to fetch deposits: ${error}`) + }) const responseData = (await response.json()) as Deposit[]