2018-08-10 12:49:26 +02:00
|
|
|
// Copyright BigchainDB GmbH and BigchainDB contributors
|
|
|
|
// SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
|
|
|
// Code is Apache-2.0 and docs are CC-BY-4.0
|
|
|
|
|
2017-06-12 16:57:29 +02:00
|
|
|
import baseRequest from './baseRequest'
|
|
|
|
import sanitize from './sanitize'
|
2017-04-26 15:58:19 +02:00
|
|
|
|
|
|
|
const DEFAULT_REQUEST_CONFIG = {
|
|
|
|
headers: {
|
|
|
|
'Accept': 'application/json'
|
|
|
|
}
|
2017-06-12 16:57:29 +02:00
|
|
|
}
|
2017-04-26 15:58:19 +02:00
|
|
|
|
2018-08-29 16:39:15 +02:00
|
|
|
const BACKOFF_DELAY = 500 // 0.5 seconds
|
2018-08-30 12:26:14 +02:00
|
|
|
const ERROR_FROM_SERVER = 'HTTP Error: Requested page not reachable'
|
2017-04-26 15:58:19 +02:00
|
|
|
/**
|
2018-05-14 17:14:40 +02:00
|
|
|
* @private
|
2017-05-11 17:19:07 +02:00
|
|
|
* Small wrapper around js-utility-belt's request that provides url resolving,
|
|
|
|
* default settings, and response handling.
|
2017-04-26 15:58:19 +02:00
|
|
|
*/
|
2018-08-22 10:10:09 +02:00
|
|
|
|
|
|
|
export default class Request {
|
2018-08-28 14:27:53 +02:00
|
|
|
constructor(node) {
|
2018-08-22 10:10:09 +02:00
|
|
|
this.node = node
|
|
|
|
this.backoffTime = null
|
2018-08-23 17:14:59 +02:00
|
|
|
this.retries = 0
|
|
|
|
this.connectionError = null
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
|
|
|
|
2018-08-29 16:39:15 +02:00
|
|
|
async request(urlPath, config, timeout, maxBackoffTime) {
|
|
|
|
if (!urlPath) {
|
|
|
|
return Promise.reject(new Error('Request was not given a url.'))
|
|
|
|
}
|
2018-08-22 10:10:09 +02:00
|
|
|
// Load default fetch configuration and remove any falsy query parameters
|
2021-03-09 14:20:00 +01:00
|
|
|
const requestConfig = {
|
|
|
|
...this.node.headers,
|
|
|
|
...DEFAULT_REQUEST_CONFIG,
|
|
|
|
...config,
|
2018-08-22 10:10:09 +02:00
|
|
|
query: config.query && sanitize(config.query)
|
2021-03-09 14:20:00 +01:00
|
|
|
}
|
2018-08-29 16:39:15 +02:00
|
|
|
const apiUrl = this.node.endpoint + urlPath
|
2018-08-22 10:10:09 +02:00
|
|
|
if (requestConfig.jsonBody) {
|
2021-03-09 14:20:00 +01:00
|
|
|
requestConfig.headers = { ...requestConfig.headers, 'Content-Type': 'application/json' }
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
|
|
|
|
2018-08-29 16:39:15 +02:00
|
|
|
// If connectionError occurs, a timestamp equal to now +
|
|
|
|
// `backoffTimedelta` is assigned to the object.
|
2018-08-23 17:14:59 +02:00
|
|
|
// Next time the function is called, it either
|
2018-08-22 10:10:09 +02:00
|
|
|
// waits till the timestamp is passed or raises `TimeoutError`.
|
|
|
|
// If `ConnectionError` occurs two or more times in a row,
|
|
|
|
// the retry count is incremented and the new timestamp is calculated
|
2018-08-29 16:39:15 +02:00
|
|
|
// as now + the `backoffTimedelta`
|
|
|
|
// The `backoffTimedelta` is the minimum between the default delay
|
|
|
|
// multiplied by two to the power of the
|
|
|
|
// number of retries or timeout/2 or 10. See Transport class for that
|
2018-08-22 10:10:09 +02:00
|
|
|
// If a request is successful, the backoff timestamp is removed,
|
|
|
|
// the retry count is back to zero.
|
|
|
|
|
2018-08-23 17:14:59 +02:00
|
|
|
const backoffTimedelta = this.getBackoffTimedelta()
|
2018-08-22 10:10:09 +02:00
|
|
|
|
2018-08-28 14:09:26 +02:00
|
|
|
if (timeout != null && timeout < backoffTimedelta) {
|
2018-08-23 17:14:59 +02:00
|
|
|
const errorObject = {
|
|
|
|
message: 'TimeoutError'
|
|
|
|
}
|
|
|
|
throw errorObject
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
2018-08-23 17:14:59 +02:00
|
|
|
if (backoffTimedelta > 0) {
|
|
|
|
await Request.sleep(backoffTimedelta)
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
2018-08-29 16:39:15 +02:00
|
|
|
|
|
|
|
const requestTimeout = timeout ? timeout - backoffTimedelta : timeout
|
|
|
|
return baseRequest(apiUrl, requestConfig, requestTimeout)
|
|
|
|
.then(async (res) => {
|
|
|
|
this.connectionError = null
|
|
|
|
return res.json()
|
|
|
|
})
|
2018-08-22 10:10:09 +02:00
|
|
|
.catch(err => {
|
2018-08-23 17:14:59 +02:00
|
|
|
// ConnectionError
|
|
|
|
this.connectionError = err
|
2018-08-22 10:10:09 +02:00
|
|
|
})
|
2018-08-23 17:14:59 +02:00
|
|
|
.finally(() => {
|
2018-08-28 14:09:26 +02:00
|
|
|
this.updateBackoffTime(maxBackoffTime)
|
2018-08-22 10:10:09 +02:00
|
|
|
})
|
2017-04-26 15:58:19 +02:00
|
|
|
}
|
2017-06-22 17:19:31 +02:00
|
|
|
|
2018-08-28 14:09:26 +02:00
|
|
|
updateBackoffTime(maxBackoffTime) {
|
2018-08-23 17:14:59 +02:00
|
|
|
if (!this.connectionError) {
|
2018-08-22 10:10:09 +02:00
|
|
|
this.retries = 0
|
|
|
|
this.backoffTime = null
|
2018-08-30 12:26:14 +02:00
|
|
|
} else if (this.connectionError.message === ERROR_FROM_SERVER) {
|
2018-08-29 16:39:15 +02:00
|
|
|
// If status is not a 2xx (based on Response.ok), throw error
|
|
|
|
this.retries = 0
|
|
|
|
this.backoffTime = null
|
|
|
|
throw this.connectionError
|
2018-08-22 10:10:09 +02:00
|
|
|
} else {
|
2018-08-29 16:39:15 +02:00
|
|
|
// Timeout or no connection could be stablished
|
2018-08-28 14:09:26 +02:00
|
|
|
const backoffTimedelta = Math.min(BACKOFF_DELAY * (2 ** this.retries), maxBackoffTime)
|
2018-08-23 17:14:59 +02:00
|
|
|
this.backoffTime = Date.now() + backoffTimedelta
|
2018-08-22 10:10:09 +02:00
|
|
|
this.retries += 1
|
2018-08-29 16:39:15 +02:00
|
|
|
if (this.connectionError.message === 'TimeoutError') {
|
|
|
|
throw this.connectionError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getBackoffTimedelta() {
|
|
|
|
if (!this.backoffTime) {
|
|
|
|
return 0
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
2018-08-29 16:39:15 +02:00
|
|
|
return (this.backoffTime - Date.now())
|
2018-08-22 10:10:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static sleep(ms) {
|
|
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
|
}
|
2017-04-26 15:58:19 +02:00
|
|
|
}
|