Skip to content

Commit

Permalink
Merge pull request #139 from fleet-sdk/retry-on-server-error
Browse files Browse the repository at this point in the history
Retry on server error
  • Loading branch information
arobsn authored Sep 6, 2024
2 parents ac04640 + 90ad0b8 commit 3cc0325
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-pumpkins-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/blockchain-providers": patch
---

Fix stream methods return types
5 changes: 5 additions & 0 deletions .changeset/two-parents-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fleet-sdk/blockchain-providers": patch
---

Retry on server errors
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {

async *streamUnconfirmedTransactions(
query: TransactionQuery<GraphQLUnconfirmedTransactionWhere> & SkipAndTake
): AsyncIterable<ChainProviderUnconfirmedTransaction<I>[]> {
): AsyncGenerator<ChainProviderUnconfirmedTransaction<I>[]> {
const pageSize = query.take ?? PAGE_SIZE;
const queries = buildGqlUnconfirmedTxQueries(query);

Expand Down Expand Up @@ -259,7 +259,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {

async *streamConfirmedTransactions(
query: TransactionQuery<GraphQLConfirmedTransactionWhere> & SkipAndTake
): AsyncIterable<ChainProviderConfirmedTransaction<I>[]> {
): AsyncGenerator<ChainProviderConfirmedTransaction<I>[]> {
const pageSize = query.take ?? PAGE_SIZE;
const queries = buildGqlConfirmedTxQueries(query);

Expand Down
6 changes: 3 additions & 3 deletions packages/blockchain-providers/src/types/blockchainProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ export interface IBlockchainProvider<I> {
/**
* Stream boxes.
*/
streamBoxes(query: BoxQuery<BoxWhere>): AsyncIterable<ChainProviderBox<I>[]>;
streamBoxes(query: BoxQuery<BoxWhere>): AsyncGenerator<ChainProviderBox<I>[]>;

/**
* Stream unconfirmed transactions
*/
streamUnconfirmedTransactions(
query: TransactionQuery<UnconfirmedTransactionWhere>
): AsyncIterable<ChainProviderUnconfirmedTransaction<I>[]>;
): AsyncGenerator<ChainProviderUnconfirmedTransaction<I>[]>;

/**
* Get unconfirmed transactions
Expand All @@ -163,7 +163,7 @@ export interface IBlockchainProvider<I> {
*/
streamConfirmedTransactions(
query: TransactionQuery<ConfirmedTransactionWhere>
): AsyncIterable<ChainProviderConfirmedTransaction<I>[]>;
): AsyncGenerator<ChainProviderConfirmedTransaction<I>[]>;

/**
* Get confirmed transactions
Expand Down
33 changes: 33 additions & 0 deletions packages/blockchain-providers/src/utils/networking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions packages/blockchain-providers/src/utils/networking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,31 @@ 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<T>(path: string, opt?: Partial<FetchOptions>): Promise<T> {
const url = buildURL(path, opt?.query, opt?.base);

let response: Response;
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);
}
Expand Down

0 comments on commit 3cc0325

Please sign in to comment.