forked from MetaMask/eth-json-rpc-middleware
-
Notifications
You must be signed in to change notification settings - Fork 1
/
fetch.js
147 lines (128 loc) · 4.17 KB
/
fetch.js
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
const fetch = global.fetch || require('fetch-ponyfill')().fetch
const url = require('url')
const JsonRpcError = require('json-rpc-error')
const btoa = require('btoa')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
module.exports = createFetchMiddleware
module.exports.createFetchConfigFromReq = createFetchConfigFromReq
const RETRIABLE_ERRORS = [
// ignore server overload errors
'Gateway timeout',
'ETIMEDOUT',
// ignore server sent html error pages
// or truncated json responses
'failed to parse response body',
// ignore errors where http req failed to establish
'Failed to fetch',
]
function createFetchMiddleware ({ rpcUrl, originHttpHeaderKey }) {
return createAsyncMiddleware(async (req, res, next) => {
const { fetchUrl, fetchParams } = createFetchConfigFromReq({ req, rpcUrl, originHttpHeaderKey })
// attempt request multiple times
const maxAttempts = 5
const retryInterval = 1000
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const fetchRes = await fetch(fetchUrl, fetchParams)
// check for http errrors
checkForHttpErrors(fetchRes)
// parse response body
const rawBody = await fetchRes.text()
let fetchBody
try {
fetchBody = JSON.parse(rawBody)
} catch (_) {
throw new Error(`FetchMiddleware - failed to parse response body: "${rawBody}"`)
}
const result = parseResponse(fetchRes, fetchBody)
// set result and exit retry loop
res.result = result
return
} catch (err) {
const errMsg = err.toString()
const isRetriable = RETRIABLE_ERRORS.some(phrase => errMsg.includes(phrase))
// re-throw error if not retriable
if (!isRetriable) throw err
}
// delay before retrying
await timeout(retryInterval)
}
})
}
function checkForHttpErrors (fetchRes) {
// check for errors
switch (fetchRes.status) {
case 405:
throw new JsonRpcError.MethodNotFound()
case 418:
throw createRatelimitError()
case 503:
case 504:
throw createTimeoutError()
}
}
function parseResponse (fetchRes, body) {
// check for error code
if (fetchRes.status !== 200) {
throw new JsonRpcError.InternalError(body)
}
// check for rpc error
if (body.error) throw new JsonRpcError.InternalError(body.error)
// return successful result
return body.result
}
function createFetchConfigFromReq({ req, rpcUrl, originHttpHeaderKey }) {
const parsedUrl = url.parse(rpcUrl)
const fetchUrl = normalizeUrlFromParsed(parsedUrl)
// prepare payload
const payload = Object.assign({}, req)
// extract 'origin' parameter from request
const originDomain = payload.origin
delete payload.origin
// serialize request body
const serializedPayload = JSON.stringify(payload)
// configure fetch params
const fetchParams = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: serializedPayload,
}
// encoded auth details as header (not allowed in fetch url)
if (parsedUrl.auth) {
const encodedAuth = btoa(parsedUrl.auth)
fetchParams.headers['Authorization'] = `Basic ${encodedAuth}`
}
// optional: add request origin as header
if (originHttpHeaderKey && originDomain) {
fetchParams.headers[originHttpHeaderKey] = originDomain
}
return { fetchUrl, fetchParams }
}
function normalizeUrlFromParsed(parsedUrl) {
let result = ''
result += parsedUrl.protocol
if (parsedUrl.slashes) result += '//'
result += parsedUrl.hostname
if (parsedUrl.port) {
result += `:${parsedUrl.port}`
}
result += `${parsedUrl.path}`
return result
}
function createRatelimitError () {
let msg = `Request is being rate limited.`
const err = new Error(msg)
return new JsonRpcError.InternalError(err)
}
function createTimeoutError () {
let msg = `Gateway timeout. The request took too long to process. `
msg += `This can happen when querying logs over too wide a block range.`
const err = new Error(msg)
return new JsonRpcError.InternalError(err)
}
function timeout(duration) {
return new Promise(resolve => setTimeout(resolve, duration))
}