js-bigchaindb-driver/src/request.js

119 lines
4.0 KiB
JavaScript

// 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
import baseRequest from './baseRequest'
import sanitize from './sanitize'
const DEFAULT_REQUEST_CONFIG = {
headers: {
'Accept': 'application/json'
}
}
const BACKOFF_DELAY = 500 // 0.5 seconds
const ERROR_FROM_SERVER = 'HTTP Error: Requested page not reachable'
/**
* @private
* Small wrapper around js-utility-belt's request that provides url resolving,
* default settings, and response handling.
*/
export default class Request {
constructor(node) {
this.node = node
this.backoffTime = null
this.retries = 0
this.connectionError = null
}
async request(urlPath, config, timeout, maxBackoffTime) {
if (!urlPath) {
return Promise.reject(new Error('Request was not given a url.'))
}
// Load default fetch configuration and remove any falsy query parameters
const requestConfig = {
...this.node.headers,
...DEFAULT_REQUEST_CONFIG,
...config,
query: config.query && sanitize(config.query)
}
const apiUrl = this.node.endpoint + urlPath
if (requestConfig.jsonBody) {
requestConfig.headers = { ...requestConfig.headers, 'Content-Type': 'application/json' }
}
// If connectionError occurs, a timestamp equal to now +
// `backoffTimedelta` is assigned to the object.
// Next time the function is called, it either
// 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
// 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
// If a request is successful, the backoff timestamp is removed,
// the retry count is back to zero.
const backoffTimedelta = this.getBackoffTimedelta()
if (timeout != null && timeout < backoffTimedelta) {
const errorObject = {
message: 'TimeoutError'
}
throw errorObject
}
if (backoffTimedelta > 0) {
await Request.sleep(backoffTimedelta)
}
const requestTimeout = timeout ? timeout - backoffTimedelta : timeout
return baseRequest(apiUrl, requestConfig, requestTimeout)
.then((res) => {
this.connectionError = null
return res.json()
})
.catch(err => {
// ConnectionError
this.connectionError = err
})
.finally(() => {
this.updateBackoffTime(maxBackoffTime)
})
}
updateBackoffTime(maxBackoffTime) {
if (!this.connectionError) {
this.retries = 0
this.backoffTime = null
} else if (this.connectionError.message === ERROR_FROM_SERVER) {
// If status is not a 2xx (based on Response.ok), throw error
this.retries = 0
this.backoffTime = null
throw this.connectionError
} else {
// Timeout or no connection could be stablished
const backoffTimedelta = Math.min(BACKOFF_DELAY * (2 ** this.retries), maxBackoffTime)
this.backoffTime = Date.now() + backoffTimedelta
this.retries += 1
if (this.connectionError.message === 'TimeoutError') {
throw this.connectionError
}
}
}
getBackoffTimedelta() {
if (!this.backoffTime) {
return 0
}
return (this.backoffTime - Date.now())
}
static sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
}