From 4d8f501970dff0abf8d1038490b5e593cdb34f36 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:34:53 -0300 Subject: [PATCH 1/2] retry on server error --- .changeset/two-parents-stare.md | 5 +++ .../src/utils/networking.spec.ts | 33 +++++++++++++++++++ .../src/utils/networking.ts | 22 ++++++++++--- 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 .changeset/two-parents-stare.md diff --git a/.changeset/two-parents-stare.md b/.changeset/two-parents-stare.md new file mode 100644 index 00000000..508f2cbb --- /dev/null +++ b/.changeset/two-parents-stare.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/blockchain-providers": patch +--- + +Retry on server errors diff --git a/packages/blockchain-providers/src/utils/networking.spec.ts b/packages/blockchain-providers/src/utils/networking.spec.ts index b826d27f..3fb7e330 100644 --- a/packages/blockchain-providers/src/utils/networking.spec.ts +++ b/packages/blockchain-providers/src/utils/networking.spec.ts @@ -97,6 +97,39 @@ describe("request", () => { expect(result).toEqual(mockResponse); }); + it("should retry if the response status is in the retry status codes", async () => { + const mockResponse = { data: "response" }; + + const fetchMock = vi + .spyOn(global, "fetch") + .mockResolvedValueOnce({ + ...resolveData({}), + status: 500, + statusText: "Internal Server Error" + } as unknown as Response) + .mockResolvedValueOnce(resolveData(mockResponse)); + + const parserMock = { + parse: vi.fn().mockReturnValue(mockResponse), + stringify: vi.fn().mockReturnValue(JSON.stringify(mockResponse)) + }; + + const result = await request("/api/data", { + parser: parserMock, + base: "https://example.com", + query: { param: "value" }, + retry: { attempts: 3, delay: 10 }, + httpOptions: { method: "GET" } + }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenCalledWith("https://example.com/api/data?param=value", { + method: "GET" + }); + expect(parserMock.parse).toHaveBeenCalledWith(JSON.stringify(mockResponse)); + expect(result).toEqual(mockResponse); + }); + it("should retry the request and return the parsed response on success", async () => { const mockResponse = { data: "response" }; const fetchMock = vi diff --git a/packages/blockchain-providers/src/utils/networking.ts b/packages/blockchain-providers/src/utils/networking.ts index ba1cc179..7f95ace0 100644 --- a/packages/blockchain-providers/src/utils/networking.ts +++ b/packages/blockchain-providers/src/utils/networking.ts @@ -17,6 +17,18 @@ export type FetchOptions = { httpOptions?: RequestInit; }; +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status +const RETRY_STATUS_CODES = new Set([ + 408, // Request Timeout + 409, // Conflict + 425, // Too Early (Experimental) + 429, // Too Many Requests + 500, // Internal Server Error + 502, // Bad Gateway + 503, // Service Unavailable + 504 // Gateway Timeout +]); + export async function request(path: string, opt?: Partial): Promise { const url = buildURL(path, opt?.query, opt?.base); @@ -24,10 +36,12 @@ export async function request(path: string, opt?: Partial): Pro if (opt?.retry) { const routes = some(opt.retry.fallbacks) ? [url, ...opt.retry.fallbacks] : [url]; const attempts = opt.retry.attempts; - response = await exponentialRetry( - (r) => fetch(resolveUrl(routes, attempts - r), opt.httpOptions), - opt.retry - ); + response = await exponentialRetry(async (r) => { + const response = await fetch(resolveUrl(routes, attempts - r), opt.httpOptions); + if (RETRY_STATUS_CODES.has(response.status)) throw new Error(response.statusText); + + return response; + }, opt.retry); } else { response = await fetch(url, opt?.httpOptions); } From 90ad0b808ed424f3b9cd45a7d6ab32c4561fb6b1 Mon Sep 17 00:00:00 2001 From: arobsn <87387688+arobsn@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:36:42 -0300 Subject: [PATCH 2/2] fix return types of stream methods --- .changeset/late-pumpkins-raise.md | 5 +++++ .../src/ergo-graphql/ergoGraphQLProvider.ts | 4 ++-- .../blockchain-providers/src/types/blockchainProvider.ts | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/late-pumpkins-raise.md diff --git a/.changeset/late-pumpkins-raise.md b/.changeset/late-pumpkins-raise.md new file mode 100644 index 00000000..3df1de71 --- /dev/null +++ b/.changeset/late-pumpkins-raise.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/blockchain-providers": patch +--- + +Fix stream methods return types diff --git a/packages/blockchain-providers/src/ergo-graphql/ergoGraphQLProvider.ts b/packages/blockchain-providers/src/ergo-graphql/ergoGraphQLProvider.ts index 59d4c0f1..5a922037 100644 --- a/packages/blockchain-providers/src/ergo-graphql/ergoGraphQLProvider.ts +++ b/packages/blockchain-providers/src/ergo-graphql/ergoGraphQLProvider.ts @@ -226,7 +226,7 @@ export class ErgoGraphQLProvider implements IBlockchainProvider { async *streamUnconfirmedTransactions( query: TransactionQuery & SkipAndTake - ): AsyncIterable[]> { + ): AsyncGenerator[]> { const pageSize = query.take ?? PAGE_SIZE; const queries = buildGqlUnconfirmedTxQueries(query); @@ -259,7 +259,7 @@ export class ErgoGraphQLProvider implements IBlockchainProvider { async *streamConfirmedTransactions( query: TransactionQuery & SkipAndTake - ): AsyncIterable[]> { + ): AsyncGenerator[]> { const pageSize = query.take ?? PAGE_SIZE; const queries = buildGqlConfirmedTxQueries(query); diff --git a/packages/blockchain-providers/src/types/blockchainProvider.ts b/packages/blockchain-providers/src/types/blockchainProvider.ts index 02f6aba2..3250a430 100644 --- a/packages/blockchain-providers/src/types/blockchainProvider.ts +++ b/packages/blockchain-providers/src/types/blockchainProvider.ts @@ -142,14 +142,14 @@ export interface IBlockchainProvider { /** * Stream boxes. */ - streamBoxes(query: BoxQuery): AsyncIterable[]>; + streamBoxes(query: BoxQuery): AsyncGenerator[]>; /** * Stream unconfirmed transactions */ streamUnconfirmedTransactions( query: TransactionQuery - ): AsyncIterable[]>; + ): AsyncGenerator[]>; /** * Get unconfirmed transactions @@ -163,7 +163,7 @@ export interface IBlockchainProvider { */ streamConfirmedTransactions( query: TransactionQuery - ): AsyncIterable[]>; + ): AsyncGenerator[]>; /** * Get confirmed transactions