Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backoff fix #1441

Merged
merged 9 commits into from
Dec 18, 2024
Merged
37 changes: 22 additions & 15 deletions packages/providers/src/fetch_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
}

Expand All @@ -47,7 +54,7 @@ interface JsonRpcRequest {
* @param headers HTTP headers to include with the request
* @returns Promise<any> }arsed JSON response from the HTTP request.
*/
export async function fetchJsonRpc(url: string, json: JsonRpcRequest, headers: object): Promise<any> {
export async function fetchJsonRpc(url: string, json: JsonRpcRequest, headers: object, retryConfig: object): Promise<any> {
const response = await backOff(async () => {
const res = await unfetch(url, {
method: 'POST',
Expand All @@ -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) {
Expand Down
77 changes: 27 additions & 50 deletions packages/providers/src/json-rpc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
baseEncode,
formatError,
getErrorTypeFromErrorMessage,
Logger,
parseRpcError,
ServerError,
} from '@near-js/utils';
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -378,56 +376,35 @@ export class JsonRpcProvider extends Provider {
* @param params Parameters to the method
*/
async sendJsonRpc<T>(method: string, params: object): Promise<T> {
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
Expand Down
6 changes: 3 additions & 3 deletions packages/providers/test/fetch_json.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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');
});
});
14 changes: 7 additions & 7 deletions packages/providers/test/fetch_json_error.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 }));
});
Expand All @@ -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 }));
});
Expand All @@ -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 }));
});
Expand All @@ -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 }));
});
Expand Down
Loading