2023-03-20 17:50:52 +01:00
/* eslint-disable jest/require-top-level-describe, jest/no-export */
import {
2023-04-14 18:21:29 +02:00
ProviderType ,
2023-03-20 17:50:52 +01:00
waitForPromiseToBeFulfilledAfterRunningAllTimers ,
withMockedCommunications ,
withNetworkClient ,
} from './helpers' ;
import {
buildFetchFailedErrorMessage ,
buildInfuraClientRetriesExhaustedErrorMessage ,
buildJsonRpcEngineEmptyResponseErrorMessage ,
} from './shared-tests' ;
2023-04-14 18:21:29 +02:00
type TestsForRpcMethodAssumingNoBlockParamOptions = {
providerType : ProviderType ;
numberOfParameters : number ;
} ;
2023-03-20 17:50:52 +01:00
/ * *
* Defines tests which exercise the behavior exhibited by an RPC method which is
* assumed to not take a block parameter . Even if it does , the value of this
* parameter will not be used in determining how to cache the method .
*
* @param method - The name of the RPC method under test .
* @param additionalArgs - Additional arguments .
* @param additionalArgs . numberOfParameters - The number of parameters supported by the method under test .
* @param additionalArgs . providerType - The type of provider being tested ;
* either ` infura ` or ` custom ` ( default : "infura" ) .
* /
export function testsForRpcMethodAssumingNoBlockParam (
2023-04-14 18:21:29 +02:00
method : string ,
{
numberOfParameters ,
providerType ,
} : TestsForRpcMethodAssumingNoBlockParamOptions ,
2023-03-20 17:50:52 +01:00
) {
if ( providerType !== 'infura' && providerType !== 'custom' ) {
throw new Error (
` providerType must be either "infura" or "custom", was " ${ providerType } " instead ` ,
) ;
}
it ( 'does not hit the RPC endpoint more than once for identical requests' , async ( ) = > {
const requests = [ { method } , { method } ] ;
const mockResults = [ 'first result' , 'second result' ] ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request : requests [ 0 ] ,
response : { result : mockResults [ 0 ] } ,
} ) ;
const results = await withNetworkClient (
{ providerType } ,
( { makeRpcCallsInSeries } ) = > makeRpcCallsInSeries ( requests ) ,
) ;
expect ( results ) . toStrictEqual ( [ mockResults [ 0 ] , mockResults [ 0 ] ] ) ;
} ) ;
} ) ;
for ( const paramIndex of [ . . . Array ( numberOfParameters ) . keys ( ) ] ) {
it ( ` does not reuse the result of a previous request if parameter at index " ${ paramIndex } " differs ` , async ( ) = > {
const firstMockParams = [
. . . new Array ( numberOfParameters ) . fill ( 'some value' ) ,
] ;
const secondMockParams = firstMockParams . slice ( ) ;
secondMockParams [ paramIndex ] = 'another value' ;
const requests = [
{
method ,
params : firstMockParams ,
} ,
{ method , params : secondMockParams } ,
] ;
const mockResults = [ 'some result' , 'another result' ] ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request : requests [ 0 ] ,
response : { result : mockResults [ 0 ] } ,
} ) ;
comms . mockRpcCall ( {
request : requests [ 1 ] ,
response : { result : mockResults [ 1 ] } ,
} ) ;
const results = await withNetworkClient (
{ providerType } ,
( { makeRpcCallsInSeries } ) = > makeRpcCallsInSeries ( requests ) ,
) ;
expect ( results ) . toStrictEqual ( [ mockResults [ 0 ] , mockResults [ 1 ] ] ) ;
} ) ;
} ) ;
}
it ( 'hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since' , async ( ) = > {
const requests = [ { method } , { method } ] ;
const mockResults = [ 'first result' , 'second result' ] ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// Note that we have to mock these requests in a specific order. The
// first block tracker request occurs because of the first RPC request.
// The second block tracker request, however, does not occur because of
// the second RPC request, but rather because we call `clock.runAll()`
// below.
comms . mockNextBlockTrackerRequest ( { blockNumber : '0x1' } ) ;
comms . mockRpcCall ( {
request : requests [ 0 ] ,
response : { result : mockResults [ 0 ] } ,
} ) ;
comms . mockNextBlockTrackerRequest ( { blockNumber : '0x2' } ) ;
comms . mockRpcCall ( {
request : requests [ 1 ] ,
response : { result : mockResults [ 1 ] } ,
} ) ;
const results = await withNetworkClient (
{ providerType } ,
async ( client ) = > {
const firstResult = await client . makeRpcCall ( requests [ 0 ] ) ;
// Proceed to the next iteration of the block tracker so that a new
// block is fetched and the current block is updated.
client . clock . runAll ( ) ;
const secondResult = await client . makeRpcCall ( requests [ 1 ] ) ;
return [ firstResult , secondResult ] ;
} ,
) ;
expect ( results ) . toStrictEqual ( mockResults ) ;
} ) ;
} ) ;
for ( const emptyValue of [ null , undefined , '\u003cnil\u003e' ] ) {
it ( ` does not retry an empty response of " ${ emptyValue } " ` , async ( ) = > {
const request = { method } ;
const mockResult = emptyValue ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : { result : mockResult } ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
expect ( result ) . toStrictEqual ( mockResult ) ;
} ) ;
} ) ;
it ( ` does not reuse the result of a previous request if it was " ${ emptyValue } " ` , async ( ) = > {
const requests = [ { method } , { method } ] ;
const mockResults = [ emptyValue , 'some result' ] ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request : requests [ 0 ] ,
response : { result : mockResults [ 0 ] } ,
} ) ;
comms . mockRpcCall ( {
request : requests [ 1 ] ,
response : { result : mockResults [ 1 ] } ,
} ) ;
const results = await withNetworkClient (
{ providerType } ,
( { makeRpcCallsInSeries } ) = > makeRpcCallsInSeries ( requests ) ,
) ;
expect ( results ) . toStrictEqual ( mockResults ) ;
} ) ;
} ) ;
}
it ( 'queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request' , async ( ) = > {
const requests = [ { method } , { method } , { method } ] ;
const mockResults = [ 'first result' , 'second result' , 'third result' ] ;
await withMockedCommunications ( { providerType } , async ( comms ) = > {
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request : requests [ 0 ] ,
response : { result : mockResults [ 0 ] } ,
delay : 100 ,
} ) ;
comms . mockRpcCall ( {
request : requests [ 1 ] ,
response : { result : mockResults [ 1 ] } ,
} ) ;
comms . mockRpcCall ( {
request : requests [ 2 ] ,
response : { result : mockResults [ 2 ] } ,
} ) ;
const results = await withNetworkClient (
{ providerType } ,
async ( client ) = > {
const resultPromises = [
client . makeRpcCall ( requests [ 0 ] ) ,
client . makeRpcCall ( requests [ 1 ] ) ,
client . makeRpcCall ( requests [ 2 ] ) ,
] ;
const firstResult = await resultPromises [ 0 ] ;
// The inflight cache middleware uses setTimeout to run the handlers,
// so run them now
client . clock . runAll ( ) ;
const remainingResults = await Promise . all ( resultPromises . slice ( 1 ) ) ;
return [ firstResult , . . . remainingResults ] ;
} ,
) ;
expect ( results ) . toStrictEqual ( [
mockResults [ 0 ] ,
mockResults [ 0 ] ,
mockResults [ 0 ] ,
] ) ;
} ) ;
} ) ;
it ( 'throws a custom error if the request to the RPC endpoint returns a 405 response' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
httpStatus : 405 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
'The method does not exist / is not available' ,
) ;
} ) ;
} ) ;
// There is a difference in how we are testing the Infura middleware vs. the
// custom RPC middleware (or, more specifically, the fetch middleware) because
// of what both middleware treat as rate limiting errors. In this case, the
// fetch middleware treats a 418 response from the RPC endpoint as such an
// error, whereas to the Infura middleware, it is a 429 response.
if ( providerType === 'infura' ) {
it ( 'throws an undescriptive error if the request to the RPC endpoint returns a 418 response' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { id : 123 , method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
httpStatus : 418 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
'{"id":123,"jsonrpc":"2.0"}' ,
) ;
} ) ;
} ) ;
it ( 'throws an error with a custom message if the request to the RPC endpoint returns a 429 response' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
httpStatus : 429 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
'Request is being rate limited' ,
) ;
} ) ;
} ) ;
} else {
it ( 'throws a custom error if the request to the RPC endpoint returns a 418 response' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
httpStatus : 418 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
'Request is being rate limited.' ,
) ;
} ) ;
} ) ;
it ( 'throws an undescriptive error if the request to the RPC endpoint returns a 429 response' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
httpStatus : 429 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
"Non-200 status code: '429'" ,
) ;
} ) ;
} ) ;
}
it ( 'throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
id : 12345 ,
jsonrpc : '2.0' ,
error : 'some error' ,
httpStatus : 420 ,
} ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
const errorMessage =
providerType === 'infura'
? '{"id":12345,"jsonrpc":"2.0","error":"some error"}'
: "Non-200 status code: '420'" ;
await expect ( promiseForResult ) . rejects . toThrow ( errorMessage ) ;
} ) ;
} ) ;
[ 503 , 504 ] . forEach ( ( httpStatus ) = > {
it ( ` retries the request to the RPC endpoint up to 5 times if it returns a ${ httpStatus } response, returning the successful result if there is one on the 5th try ` , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
response : {
error : 'Some error' ,
httpStatus ,
} ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
it ( ` causes a request to fail with a custom error if the request to the RPC endpoint returns a ${ httpStatus } response 5 times in a row ` , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
response : {
error : 'Some error' ,
httpStatus ,
} ,
times : 5 ,
} ) ;
comms . mockNextBlockTrackerRequest ( ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
const err =
providerType === 'infura'
? buildInfuraClientRetriesExhaustedErrorMessage ( 'Gateway timeout' )
: buildJsonRpcEngineEmptyResponseErrorMessage ( method ) ;
await expect ( promiseForResult ) . rejects . toThrow ( err ) ;
} ) ;
} ) ;
} ) ;
it ( 'retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
error : 'ETIMEDOUT: Some message' ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
// Both the Infura and fetch middleware detect ETIMEDOUT errors and will
// automatically retry the request to the RPC endpoint in question, but both
// produce a different error if the number of retries is exhausted.
if ( providerType === 'infura' ) {
it ( 'causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'ETIMEDOUT: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildInfuraClientRetriesExhaustedErrorMessage ( errorMessage ) ,
) ;
} ) ;
} ) ;
} else {
it ( 'returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'ETIMEDOUT: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildJsonRpcEngineEmptyResponseErrorMessage ( method ) ,
) ;
} ) ;
} ) ;
}
// The Infura middleware treats a response that contains an ECONNRESET message
// as an innocuous error that is likely to disappear on a retry. The custom
// RPC middleware, on the other hand, does not specially handle this error.
if ( providerType === 'infura' ) {
it ( 'retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
error : 'ECONNRESET: Some message' ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
it ( 'causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'ECONNRESET: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildInfuraClientRetriesExhaustedErrorMessage ( errorMessage ) ,
) ;
} ) ;
} ) ;
} else {
it ( 'does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request' , async ( ) = > {
const customRpcUrl = 'http://example.com' ;
await withMockedCommunications (
{ providerType , customRpcUrl } ,
async ( comms ) = > {
const request = { method } ;
const errorMessage = 'ECONNRESET: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType , customRpcUrl } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildFetchFailedErrorMessage ( customRpcUrl , errorMessage ) ,
) ;
} ,
) ;
} ) ;
}
// Both the Infura and fetch middleware will attempt to parse the response
// body as JSON, and if this step produces an error, both middleware will also
// attempt to retry the request. However, this error handling code is slightly
// different between the two. As the error in this case is a SyntaxError, the
// Infura middleware will catch it immediately, whereas the custom RPC
// middleware will catch it and re-throw a separate error, which it then
// catches later.
if ( providerType === 'infura' ) {
it ( 'retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
error : 'SyntaxError: Some message' ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
it ( 'causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'SyntaxError: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildInfuraClientRetriesExhaustedErrorMessage ( errorMessage ) ,
) ;
} ) ;
} ) ;
it ( 'does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'failed to parse response body: some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType , infuraNetwork : comms.infuraNetwork } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildFetchFailedErrorMessage ( comms . rpcUrl , errorMessage ) ,
) ;
} ) ;
} ) ;
} else {
it ( 'does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request' , async ( ) = > {
const customRpcUrl = 'http://example.com' ;
await withMockedCommunications (
{ providerType , customRpcUrl } ,
async ( comms ) = > {
const request = { method } ;
const errorMessage = 'SyntaxError: Some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType , customRpcUrl } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildFetchFailedErrorMessage ( customRpcUrl , errorMessage ) ,
) ;
} ,
) ;
} ) ;
it ( 'retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
error : 'failed to parse response body: some message' ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
it ( 'returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'failed to parse response body: some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildJsonRpcEngineEmptyResponseErrorMessage ( method ) ,
) ;
} ) ;
} ) ;
}
// Only the custom RPC middleware will detect a "Failed to fetch" error and
// attempt to retry the request to the RPC endpoint; the Infura middleware
// does not.
if ( providerType === 'infura' ) {
it ( 'does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'Failed to fetch: some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType , infuraNetwork : comms.infuraNetwork } ,
async ( { makeRpcCall } ) = > makeRpcCall ( request ) ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildFetchFailedErrorMessage ( comms . rpcUrl , errorMessage ) ,
) ;
} ) ;
} ) ;
} else {
it ( 'retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
// Here we have the request fail for the first 4 tries, then succeed
// on the 5th try.
comms . mockRpcCall ( {
request ,
error : 'Failed to fetch: some message' ,
times : 4 ,
} ) ;
comms . mockRpcCall ( {
request ,
response : {
result : 'the result' ,
httpStatus : 200 ,
} ,
} ) ;
const result = await withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
expect ( result ) . toStrictEqual ( 'the result' ) ;
} ) ;
} ) ;
it ( 'returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row' , async ( ) = > {
await withMockedCommunications ( { providerType } , async ( comms ) = > {
const request = { method } ;
const errorMessage = 'Failed to fetch: some message' ;
// The first time a block-cacheable request is made, the latest block
// number is retrieved through the block tracker first. It doesn't
// matter what this is — it's just used as a cache key.
comms . mockNextBlockTrackerRequest ( ) ;
comms . mockRpcCall ( {
request ,
error : errorMessage ,
times : 5 ,
} ) ;
const promiseForResult = withNetworkClient (
{ providerType } ,
async ( { makeRpcCall , clock } ) = > {
return await waitForPromiseToBeFulfilledAfterRunningAllTimers (
makeRpcCall ( request ) ,
clock ,
) ;
} ,
) ;
await expect ( promiseForResult ) . rejects . toThrow (
buildJsonRpcEngineEmptyResponseErrorMessage ( method ) ,
) ;
} ) ;
} ) ;
}
}