diff --git a/packages/providers/src/fetch_json.ts b/packages/providers/src/fetch_json.ts index 1c68594a8..3ee6d1af2 100644 --- a/packages/providers/src/fetch_json.ts +++ b/packages/providers/src/fetch_json.ts @@ -4,22 +4,26 @@ import unfetch from 'isomorphic-unfetch'; const BACKOFF_MULTIPLIER = 1.5; const RETRY_NUMBER = 10; +const RETRY_DELAY = 0; -const retryConfig = { - numOfAttempts: RETRY_NUMBER, - timeMultiple: BACKOFF_MULTIPLIER, - retry: (e: ProviderError) => { - if ([503, 500, 408].includes(e.cause)) { - return true; - } +export function retryConfig(numOfAttempts=RETRY_NUMBER, timeMultiple=BACKOFF_MULTIPLIER, startingDelay=RETRY_DELAY) { + return { + numOfAttempts: numOfAttempts, + timeMultiple: timeMultiple, + startingDelay: startingDelay, + retry: (e: ProviderError) => { + if ([503, 500, 408].includes(e.cause)) { + return true; + } - if (e.toString().includes('FetchError') || e.toString().includes('Failed to fetch')) { - return true; - } + if (e.toString().includes('FetchError') || e.toString().includes('Failed to fetch')) { + return true; + } - return false; - } -}; + return false; + } + }; +} export interface ConnectionInfo { url: string; @@ -30,6 +34,9 @@ export class ProviderError extends Error { cause: number; constructor(message: string, options: any) { super(message, options); + if (options.cause) { + this.cause = options.cause; + } } } @@ -47,7 +54,7 @@ interface JsonRpcRequest { * @param headers HTTP headers to include with the request * @returns Promise }arsed JSON response from the HTTP request. */ -export async function fetchJsonRpc(url: string, json: JsonRpcRequest, headers: object): Promise { +export async function fetchJsonRpc(url: string, json: JsonRpcRequest, headers: object, retryConfig: object): Promise { const response = await backOff(async () => { const res = await unfetch(url, { method: 'POST', @@ -58,7 +65,7 @@ export async function fetchJsonRpc(url: string, json: JsonRpcRequest, headers: o const { ok, status } = res; if (status === 500) { - throw new ProviderError(`Internal server error`, { cause: status }); + throw new ProviderError('Internal server error', { cause: status }); } else if (status === 408) { throw new ProviderError('Timeout error', { cause: status }); } else if (status === 400) { diff --git a/packages/providers/src/json-rpc-provider.ts b/packages/providers/src/json-rpc-provider.ts index 040045cc4..0cbe3eb5e 100644 --- a/packages/providers/src/json-rpc-provider.ts +++ b/packages/providers/src/json-rpc-provider.ts @@ -9,7 +9,6 @@ import { baseEncode, formatError, getErrorTypeFromErrorMessage, - Logger, parseRpcError, ServerError, } from '@near-js/utils'; @@ -39,9 +38,8 @@ import { SignedTransaction, } from '@near-js/transactions'; -import { exponentialBackoff } from './exponential-backoff'; import { Provider } from './provider'; -import { ConnectionInfo, fetchJsonRpc } from './fetch_json'; +import { ConnectionInfo, fetchJsonRpc, retryConfig } from './fetch_json'; import { TxExecutionStatus } from '@near-js/types'; /** @hidden */ @@ -378,56 +376,35 @@ export class JsonRpcProvider extends Provider { * @param params Parameters to the method */ async sendJsonRpc(method: string, params: object): Promise { - const response = await exponentialBackoff(this.options.wait, this.options.retries, this.options.backoff, async () => { - try { - const request = { - method, - params, - id: (_nextId++), - jsonrpc: '2.0' - }; - const response = await fetchJsonRpc(this.connection.url, request, this.connection.headers); - if (response.error) { - if (typeof response.error.data === 'object') { - if (typeof response.error.data.error_message === 'string' && typeof response.error.data.error_type === 'string') { - // if error data has error_message and error_type properties, we consider that node returned an error in the old format - throw new TypedError(response.error.data.error_message, response.error.data.error_type); - } - - throw parseRpcError(response.error.data); - } else { - const errorMessage = `[${response.error.code}] ${response.error.message}: ${response.error.data}`; - // NOTE: All this hackery is happening because structured errors not implemented - // TODO: Fix when https://github.com/nearprotocol/nearcore/issues/1839 gets resolved - if (response.error.data === 'Timeout' || errorMessage.includes('Timeout error') - || errorMessage.includes('query has timed out')) { - throw new TypedError(errorMessage, 'TimeoutError'); - } - - const errorType = getErrorTypeFromErrorMessage(response.error.data, ''); - if (errorType) { - throw new TypedError(formatError(errorType, params), errorType); - } - throw new TypedError(errorMessage, response.error.name); - } - } else if (typeof response.result?.error === 'string') { - const errorType = getErrorTypeFromErrorMessage(response.result.error, ''); - - if (errorType) { - throw new ServerError(formatError(errorType, params), errorType); - } - } - // Success when response.error is not exist - return response; - } catch (error) { - if (error.type === 'TimeoutError') { - Logger.warn(`Retrying request to ${method} as it has timed out`, params); - return null; + const request = { + method, + params, + id: (_nextId++), + jsonrpc: '2.0' + }; + const response = await fetchJsonRpc(this.connection.url, request, this.connection.headers, retryConfig(this.options.retries, this.options.backoff, this.options.wait)); + if (response.error) { + if (typeof response.error.data === 'object') { + if (typeof response.error.data.error_message === 'string' && typeof response.error.data.error_type === 'string') { + // if error data has error_message and error_type properties, we consider that node returned an error in the old format + throw new TypedError(response.error.data.error_message, response.error.data.error_type); } + throw parseRpcError(response.error.data); + } else { + const errorMessage = `[${response.error.code}] ${response.error.message}: ${response.error.data}`; - throw error; + const errorType = getErrorTypeFromErrorMessage(response.error.data, ''); + if (errorType) { + throw new TypedError(formatError(errorType, params), errorType); + } + throw new TypedError(errorMessage, response.error.name); } - }); + } else if (typeof response.result?.error === 'string') { + const errorType = getErrorTypeFromErrorMessage(response.result.error, ''); + if (errorType) { + throw new ServerError(formatError(errorType, params), errorType); + } + } const { result } = response; // From jsonrpc spec: // result diff --git a/packages/providers/test/fetch_json.test.ts b/packages/providers/test/fetch_json.test.ts index a4b479b9d..b87a6bf8f 100644 --- a/packages/providers/test/fetch_json.test.ts +++ b/packages/providers/test/fetch_json.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from '@jest/globals'; -import { fetchJsonRpc } from '../src/fetch_json'; +import { fetchJsonRpc, retryConfig } from '../src/fetch_json'; describe('fetchJson', () => { test('string parameter in fetchJson', async () => { @@ -11,7 +11,7 @@ describe('fetchJson', () => { params: [] }; // @ts-expect-error test input - const result = await fetchJsonRpc(RPC_URL, statusRequest, undefined); + const result = await fetchJsonRpc(RPC_URL, statusRequest, undefined, retryConfig()); expect(result.result.chain_id).toBe('testnet'); }); test('object parameter in fetchJson', async () => { @@ -23,7 +23,7 @@ describe('fetchJson', () => { params: [] }; // @ts-expect-error test input - const result = await fetchJsonRpc(connection.url, statusRequest, undefined); + const result = await fetchJsonRpc(connection.url, statusRequest, undefined, retryConfig()); expect(result.result.chain_id).toBe('testnet'); }); }); diff --git a/packages/providers/test/fetch_json_error.test.ts b/packages/providers/test/fetch_json_error.test.ts index 1e65564f1..21b9bfe5c 100644 --- a/packages/providers/test/fetch_json_error.test.ts +++ b/packages/providers/test/fetch_json_error.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, jest, afterEach } from '@jest/globals'; -import { fetchJsonRpc } from '../src/fetch_json'; +import { fetchJsonRpc, retryConfig } from '../src/fetch_json'; import unfetch from 'isomorphic-unfetch'; import { ProviderError } from '../src/fetch_json'; @@ -27,7 +27,7 @@ describe('fetchJsonError', () => { }); // @ts-expect-error test input - await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined)) + await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined, retryConfig())) .rejects .toThrowError(new ProviderError('Internal server error', { cause: 500 })); }); @@ -39,7 +39,7 @@ describe('fetchJsonError', () => { json: {}, }); // @ts-expect-error test input - await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined)) + await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined, retryConfig())) .rejects .toThrowError(new ProviderError('Timeout error', { cause: 408 })); }); @@ -52,8 +52,8 @@ describe('fetchJsonError', () => { text: '', json: {}, }); - // @ts-expect-error test input - await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined)) + // @ts-expect-error test input + await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined, retryConfig())) .rejects .toThrowError(new ProviderError('Request validation error', { cause: 400 })); }); @@ -66,8 +66,8 @@ describe('fetchJsonError', () => { json: {}, }); - // @ts-expect-error test input - await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined)) + // @ts-expect-error test input + await expect(fetchJsonRpc(RPC_URL, statusRequest, undefined, retryConfig())) .rejects .toThrowError(new ProviderError(`${RPC_URL} unavailable`, { cause: 503 })); });