-
-
Notifications
You must be signed in to change notification settings - Fork 54
/
create-infura-middleware.ts
247 lines (228 loc) · 7.58 KB
/
create-infura-middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import type { PendingJsonRpcResponse } from 'json-rpc-engine';
import { createAsyncMiddleware } from 'json-rpc-engine';
import type { EthereumRpcError } from 'eth-rpc-errors';
import { ethErrors } from 'eth-rpc-errors';
import fetch from 'node-fetch';
import type {
ExtendedJsonRpcRequest,
InfuraJsonRpcSupportedNetwork,
RequestHeaders,
} from './types';
import { fetchConfigFromReq } from './fetch-config-from-req';
import { projectLogger, createModuleLogger } from './logging-utils';
export interface CreateInfuraMiddlewareOptions {
network?: InfuraJsonRpcSupportedNetwork;
maxAttempts?: number;
source?: string;
projectId: string;
headers?: Record<string, string>;
}
const log = createModuleLogger(projectLogger, 'create-infura-middleware');
const RETRIABLE_ERRORS = [
// ignore server overload errors
'Gateway timeout',
'ETIMEDOUT',
'ECONNRESET',
// ignore server sent html error pages
// or truncated json responses
'SyntaxError',
];
/**
* Builds [`json-rpc-engine`](https://github.com/MetaMask/json-rpc-engine)-compatible middleware designed
* for interfacing with Infura's JSON-RPC endpoints.
*
* @param opts - The options.
* @param opts.network - A network that Infura supports; plugs into
* `https://${network}.infura.io` (default: 'mainnet').
* @param opts.maxAttempts - The number of times a request to Infura should be
* retried in the case of failure (default: 5).
* @param opts.source - A descriptor for the entity making the request; tracked
* by Infura for analytics purposes.
* @param opts.projectId - The Infura project id.
* @param opts.headers - Extra headers that will be used to make the request.
* @returns The `json-rpc-engine`-compatible middleware.
*/
export function createInfuraMiddleware({
network = 'mainnet',
maxAttempts = 5,
source,
projectId,
headers = {},
}: CreateInfuraMiddlewareOptions) {
// validate options
if (!projectId || typeof projectId !== 'string') {
throw new Error(`Invalid value for 'projectId': "${projectId}"`);
}
if (!headers || typeof headers !== 'object') {
throw new Error(`Invalid value for 'headers': "${headers}"`);
}
if (!maxAttempts) {
throw new Error(
`Invalid value for 'maxAttempts': "${maxAttempts}" (${typeof maxAttempts})`,
);
}
return createAsyncMiddleware(async (req, res) => {
// retry MAX_ATTEMPTS times, if error matches filter
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
// attempt request
log(
'Attempting request to Infura. network = %o, projectId = %s, headers = %o, req = %o',
network,
projectId,
headers,
req,
);
await performFetch(network, projectId, headers, req, res, source);
// request was successful
break;
} catch (err: any) {
// an error was caught while performing the request
// if not retriable, resolve with the encountered error
if (!isRetriableError(err)) {
// abort with error
log(
'Non-retriable request error encountered. req = %o, res = %o, error = %o',
req,
res,
err,
);
throw err;
}
// if no more attempts remaining, throw an error
const remainingAttempts = maxAttempts - attempt;
if (!remainingAttempts) {
log(
'Retriable request error encountered, but exceeded max attempts. req = %o, res = %o, error = %o',
req,
res,
err,
);
const errMsg = `InfuraProvider - cannot complete request. All retries exhausted.\nOriginal Error:\n${err.toString()}\n\n`;
const retriesExhaustedErr = new Error(errMsg);
throw retriesExhaustedErr;
}
// otherwise, ignore error and retry again after timeout
log(
'Retriable request error encountered. req = %o, res = %o, error = %o',
req,
res,
err,
);
log('Waiting 1 second to try again...');
await timeout(1000);
}
}
// request was handled correctly, end
});
}
/**
* Makes a request to Infura, updating the given response object if the response
* has a "successful" status code or throwing an error otherwise.
*
* @param network - A network that Infura supports; plugs into
* `https://${network}.infura.io`.
* @param projectId - The Infura project id.
* @param extraHeaders - Extra headers that will be used to make the request.
* @param req - The original request object obtained via the middleware stack.
* @param res - The original response object obtained via the middleware stack.
* @param source - A descriptor for the entity making the request;
* tracked by Infura for analytics purposes.
* @throws an error with a detailed message if the HTTP status code is anywhere
* outside 2xx, and especially if it is 405, 429, 503, or 504.
*/
async function performFetch(
network: InfuraJsonRpcSupportedNetwork,
projectId: string,
extraHeaders: RequestHeaders,
req: ExtendedJsonRpcRequest<unknown>,
res: PendingJsonRpcResponse<unknown>,
source: string | undefined,
): Promise<void> {
const { fetchUrl, fetchParams } = fetchConfigFromReq({
network,
projectId,
extraHeaders,
req,
source,
});
const response = await fetch(fetchUrl, fetchParams);
const rawData = await response.text();
// handle errors
if (!response.ok) {
switch (response.status) {
case 405:
throw ethErrors.rpc.methodNotFound();
case 429:
throw createRatelimitError();
case 503:
case 504:
throw createTimeoutError();
default:
throw createInternalError(rawData);
}
}
// special case for now
if (req.method === 'eth_getBlockByNumber' && rawData === 'Not Found') {
res.result = null;
return;
}
// parse JSON
const data = JSON.parse(rawData);
// finally return result
res.result = data.result;
res.error = data.error;
}
/**
* Builds a JSON-RPC 2.0 internal error object describing a rate-limiting
* error.
*
* @returns The error object.
*/
function createRatelimitError(): EthereumRpcError<undefined> {
const msg = `Request is being rate limited.`;
return createInternalError(msg);
}
/**
* Builds a JSON-RPC 2.0 internal error object describing a timeout error.
*
* @returns The error object.
*/
function createTimeoutError(): EthereumRpcError<undefined> {
let msg = `Gateway timeout. The request took too long to process. `;
msg += `This can happen when querying logs over too wide a block range.`;
return createInternalError(msg);
}
/**
* Builds a JSON-RPC 2.0 internal error object.
*
* @param msg - The message.
* @returns The error object.
*/
function createInternalError(msg: string): EthereumRpcError<undefined> {
return ethErrors.rpc.internal(msg);
}
/**
* Upon making a request, we may get an error that is temporary and
* intermittent. In these cases we can attempt the request again with the
* assumption that the error is unlikely to occur again. Here we determine if we
* have received such an error.
*
* @param err - The error object.
* @returns Whether the request that produced the error can be retried.
*/
function isRetriableError(err: any): boolean {
const errMessage = err.toString();
return RETRIABLE_ERRORS.some((phrase) => errMessage.includes(phrase));
}
/**
* A utility function that promisifies `setTimeout`.
*
* @param length - The number of milliseconds to wait.
* @returns A promise that resolves after the given time has elapsed.
*/
function timeout(length: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, length);
});
}