@ama-team/voxengine-sdk
Version:
Bundle of helper functions and classes for VoxEngine scenario scripts
305 lines (281 loc) • 8.87 kB
JavaScript
/* global Net */
var timeout = require('../concurrent').timeout
var C = require('./_common')
var Q = C.Query
var H = C.Headers
var Slf4j = require('../logger').Slf4j
/**
* Provides client configuration defaults.
*
* @returns {BasicHttpClientSettings}
*/
function getDefaults () {
return {
url: '',
retryOnNetworkError: true,
throwOnServerError: true,
retryOnServerError: true,
throwOnClientError: true,
retryOnClientError: false,
throwOnNotFound: false,
retryOnNotFound: false,
retries: 4,
logger: {}
}
}
/**
* Internal response type, used to simplify processing path finding.
*
* RS is an abbreviation from ResponseStatus
*
* @enum
* @readonly
*/
var RS = {
NetworkError: 'NetworkError',
ServerError: 'ServerError',
ClientError: 'ClientError',
NotFound: 'NotFound',
Ok: 'Ok',
fromCode: function (code) {
if (code < 200 || !code) {
return RS.NetworkError
}
if (code < 400) {
return RS.Ok
}
if (code === 404) {
return RS.NotFound
}
if (code < 500) {
return RS.ClientError
}
return RS.ServerError
}
}
/**
* @typedef {Object} BasicHttpClientSettings
*
* @property {string} url
* @property {boolean} retryOnNetworkError
* @property {boolean} throwOnServerError
* @property {boolean} retryOnServerError
* @property {boolean} throwOnClientError
* @property {boolean} retryOnClientError
* @property {boolean} throwOnNotFound
* @property {boolean} retryOnNotFound
* @property {string} methodOverrideHeader
* @property {HeaderBag} headers Headers to be used on every request.
* @property {int} retries Maximum number of retries allowed for request.
* @property {LoggerOptions} logger Logger instance or context and/or name.
* @property {int} timeout
*/
/**
* @class
*
* @implements IHttpClient
*
* @param {BasicHttpClientSettings|object} [settings]
* @param {netHttpRequestAsync} [transport]
*/
function BasicHttpClient (settings, transport) {
transport = transport || Net.httpRequestAsync
settings = settings || {}
if (typeof settings === 'string' || settings instanceof String) {
settings = {url: settings}
}
var defaults = getDefaults()
var logger = Slf4j.factory(setting('logger'), 'ama-team.voxengine-sdk.http.basic')
var self = this
var requests = 0
function fetch (source, key, def) {
return source[key] !== undefined ? source[key] : def
}
function setting (key, def) {
return fetch(settings, key, fetch(defaults, key, def))
}
function shouldRetry (status, attempt) {
// number of attempts = 1 + number of retries, so it's 'greater than' rather than 'greater or equal to'
// comparison
return attempt <= setting('retries') && setting('retryOn' + status, false)
}
function shouldThrow (status) {
return status === RS.NetworkError || setting('throwOn' + status, false)
}
function performThrow (status, request, response) {
// yes, i'm counting bytes and switch is more expensive
if (status === RS.ServerError) {
throw new C.ServerErrorException('Server returned erroneous response', request, response)
} else if (status === RS.ClientError) {
throw new C.ClientErrorException('Client has performed an invalid request', request, response)
} else if (status === RS.NotFound) {
throw new C.NotFoundException('Requested resource hasn\'t been found', request, response)
}
// get exception with specified code, otherwise use default one
var ErrorClass = C.codeExceptionIndex[response.code] || C.NetworkException
throw new ErrorClass(null, response.code, request, response)
}
/**
* Executes HTTP request.
*
* @param {HttpRequest} request
* @return {Promise.<(HttpResponse|Error)>}
*/
function execute (request) {
var message
request.timeout = typeof request.timeout === 'number' ? request.timeout : settings.timeout
if (!request.method) {
message = 'Request method hasn\'t been specified'
return Promise.reject(new C.InvalidConfigurationException(message))
}
var id = request.id = request.id || ++requests
request.method = request.method.toUpperCase()
if (['POST', 'GET'].indexOf(request.method) === -1 && !setting('methodOverrideHeader')) {
message = 'Tried to execute non-GET/POST request without specifying methodOverrideHeader in settings'
return Promise.reject(new C.InvalidConfigurationException(message))
}
request.query = Q.normalize(request.query)
request.headers = H.normalize(request.headers)
if (!request.payload) {
request.payload = null
}
var url = request.url = setting('url') + (request.url || '')
logger.debug('Executing request #{} `{} {}`', request.id, request.method, url)
return executionLoop(request, 1)
.then(function (response) {
logger.debug('Request #{} `{} {}` got response with code {}', id,
request.method, url, response.code)
return response
}, function (e) {
logger.debug('Request #{} `{} {}` resulted in error {}', id,
request.method, url, e.name)
throw e
})
}
function executionLoop (request, attempt) {
var qs = Q.encode(request.query)
var url = request.url + (qs.length > 0 ? '?' + qs : '')
var opts = new Net.HttpRequestOptions()
var method = ['HEAD', 'GET'].indexOf(request.method) === -1 ? 'POST' : 'GET'
var headers = H.override(setting('headers', {}), request.headers)
if (method !== request.method) {
headers[setting('methodOverrideHeader')] = [request.method]
}
opts.method = method
opts.postData = request.payload
opts.headers = H.encode(headers)
logger.trace('Executing request #{} `{} {}`, attempt #{}', request.id, request.method, url, attempt)
return timeout(transport(url, opts), request.timeout).then(function (raw) {
var response = {code: raw.code, headers: H.decode(raw.headers), payload: raw.text}
var status = RS.fromCode(response.code)
var toRetry = shouldRetry(status, attempt)
var toThrow = !toRetry && shouldThrow(status)
logger.trace('Request #{} `{} {}` (attempt #{}) ended with code `{}` / status `{}`, (retry: {}, throw: {})',
request.id, request.method, url, attempt, response.code, status, toRetry, toThrow)
if (toRetry) {
return executionLoop(request, attempt + 1)
}
if (toThrow) {
performThrow(status, request, response)
}
response.request = request
return response
})
}
function request (method, url, query, payload, headers, timeout) {
return execute({url: url, method: method, headers: headers, query: query, payload: payload, timeout: timeout})
}
// noinspection JSUnusedGlobalSymbols
this.execute = execute
this.request = request
var methods = ['get', 'head']
methods.forEach(function (method) {
self[method] = function (url, query, headers, timeout) {
return request(method, url, query, null, headers, timeout)
}
})
methods = ['post', 'put', 'patch', 'delete']
methods.forEach(function (method) {
self[method] = function (url, payload, headers, query, timeout) {
return request(method, url, query, payload, headers, timeout)
}
})
/**
* Perform GET request.
*
* @function BasicHttpClient#get
*
* @param {string} url
* @param {QueryBag} [query]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
/**
* Perform HEAD request.
*
* @function BasicHttpClient#head
*
* @param {string} url
* @param {QueryBag} [query]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
/**
* Perform POST request.
*
* @function BasicHttpClient#post
*
* @param {string} url
* @param {string} [payload]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
/**
* Perform PUT request.
*
* @function BasicHttpClient#put
*
* @param {string} url
* @param {string} [payload]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
/**
* Perform PATCH request.
*
* @function BasicHttpClient#patch
*
* @param {string} url
* @param {string} [payload]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
/**
* Perform DELETE request.
*
* @function BasicHttpClient#delete
*
* @param {string} url
* @param {string} [payload]
* @param {HeaderBag} [headers]
* @param {int} [timeout]
*
* @return {HttpResponsePromise}
*/
}
BasicHttpClient.getDefaults = getDefaults
module.exports = {
Client: BasicHttpClient,
/** @deprecated */
getDefaults: getDefaults
}