'use strict';
const compose = require('koa-compose');
const context = require('./context');
const rejectedPromise = require('./rejectedPromise');
const bind = require('./bind');
/** Core client */
class HttpTransportClient {
/**
* Create a HttpTransport.
* @param {Transport} transport - Transport instance.
* @param {object} defaults - default configuration
*/
constructor(transport, defaults) {
this._transport = transport;
this._instancePlugins = defaults.plugins || [];
this._defaults = defaults;
this._initContext();
bind(this);
}
/**
* Registers a per request plugin
*
* @return a HttpTransport instance
* @param {function} fn - per request plugin
* @example
* const toError = require('@bbc/http-transport-to-error');
* const httpTransport = require('@bbc/http-transport');
*
* httpTransport.createClient()
* .use(toError(404));
*/
use(plugin) {
validatePlugin(plugin);
this._ctx.addPlugin(plugin);
return this;
}
/**
* Make a HTTP GET request
*
* @param {string} baseUrl
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .get(url)
* .asResponse();
*/
get(baseUrl) {
this._ctx.req.method('GET').baseUrl(baseUrl);
return this;
}
/**
* Make a HTTP POST request
*
* @param {string} baseUrl
* @param {object} request body
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .post(baseUrl, requestBody)
* .asResponse();
*/
post(baseUrl, body) {
this._ctx.req
.method('POST')
.body(body)
.baseUrl(baseUrl);
return this;
}
/**
* Make a HTTP PUT request
*
* @param {string} baseUrl
* @param {object} request body
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .put(baseUrl, requestBody)
* .asResponse();
*/
put(baseUrl, body) {
this._ctx.req
.method('PUT')
.body(body)
.baseUrl(baseUrl);
return this;
}
/**
* Make a HTTP DELETE request
*
* @param {string} baseUrl
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .delete(baseUrl)
* .asResponse();
*/
delete(baseUrl) {
this._ctx.req.method('DELETE').baseUrl(baseUrl);
return this;
}
/**
* Make a HTTP PATCH request
*
* @param {string} baseUrl
* @param {object} request body
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .put(baseUrl, requestBody)
* .asResponse();
*/
patch(baseUrl, body) {
this._ctx.req
.method('PATCH')
.body(body)
.baseUrl(baseUrl);
return this;
}
/**
* Make a HTTP HEAD request
*
* @param {string} baseUrl
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .head(baseUrl)
* .asResponse();
*/
head(baseUrl) {
this._ctx.req.method('HEAD').baseUrl(baseUrl);
return this;
}
/**
* Sets the request headers
*
* @param {string|object} name - header name or headers object
* @param {string|object} value - header value
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .headers({
* 'User-Agent' : 'someUserAgent'
* })
* .asResponse();
*/
headers() {
const args = normalise(arguments);
Object.keys(args).forEach((key) => {
this._ctx.req.addHeader(key, args[key]);
});
return this;
}
/**
* Sets the query strings
*
* @param {string|object} name - query name or query object
* @param {string|object} value - query value
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .query({
* 'perPage' : 1
* })
* .asResponse();
*/
query() {
const args = normalise(arguments);
Object.keys(args).forEach((key) => {
this._ctx.req.addQuery(key, args[key]);
});
return this;
}
/**
* Sets a request timeout
*
* @param {integer} time - timeout in seconds
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .timeout(1)
* .asResponse();
*/
timeout(time) {
this._ctx.req.timeout(time);
return this;
}
/**
* Set the redirect handling:
* `follow` (default) to follow the redirects automatically,
* `manual` to extract redirect headers,
* `error` to reject redirect
*
* @param {'follow'|'manual'|'error'} redirect - redirect handling
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .redirect('manual') // for this request only
* .asResponse();
*/
redirect(redirectType) {
this._ctx.req.redirect(redirectType);
return this;
}
/**
* Set the number of retries on failure for the request
*
* @param {integer} retries - number of times to retry a failed request
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .retry(5) // for this request only
* .asResponse();
*/
retry(retries) {
this._ctx.retries = retries;
return this;
}
/**
* Set the delay between retries in ms
*
* @param {integer} delay - number of ms to wait between retries (default: 100)
* @return a HttpTransport instance
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .retry(2)
* .retryDelay(200)
* .asResponse();
*/
retryDelay(delay) {
this._ctx.retryDelay = delay;
return this;
}
/**
* Initiates the request, returning the response body, if successful.
*
* @return a Promise. If the Promise fulfils,
* the fulfilment value is the response body. The body type defaults to string.
* If the content-type response header contains 'json'
* or the json: true option has been set on transport layer
* then the body type will be json.
*
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const body = await httpTransport.createClient()
* .asBody();
*
* console.log(body);
*/
async asBody() {
const res = await this.asResponse();
return res.body;
}
/**
* Initiates the request, returning a http transport response object, if successful.
*
* @return a Promise. If the Promise fulfils,
* the fulfilment value is response object.
* @example
* const httpTransport = require('@bbc/http-transport');
*
* const response = await httpTransport.createClient()
* .asResponse()
*
* console.log(response);
*
*/
async asResponse() {
const currentContext = this._ctx;
this._initContext();
const ctx = await retry(this._executeRequest, currentContext);
return ctx.res;
}
_getPlugins(ctx) {
return this._instancePlugins.concat(ctx.plugins);
}
_applyPlugins(ctx, next) {
const fn = compose(this._getPlugins(ctx));
return fn(ctx, next);
}
async _executeRequest(ctx) {
await this._applyPlugins(ctx, this._handleRequest);
return ctx;
}
async _handleRequest(ctx, next) {
await this._transport.execute(ctx);
return next();
}
_initContext() {
this._ctx = context.create(this._defaults);
this.headers('User-Agent', this._ctx.userAgent);
}
}
function isCriticalError(err) {
if (err && err.statusCode < 500) {
return false;
}
return true;
}
function toRetry(err) {
return {
reason: err.message,
statusCode: err.statusCode
};
}
function retry(fn, ctx) {
ctx.retryAttempts = [];
const maxAttempts = ctx.retries;
function attempt(i) {
return fn(ctx)
.catch((err) => {
if (maxAttempts > 0) {
const delayBy = rejectedPromise(ctx.retryDelay);
return delayBy(err);
}
throw err;
})
.catch((err) => {
if (i < maxAttempts && isCriticalError(err)) {
ctx.retryAttempts.push(toRetry(err));
return attempt(++i);
}
throw err;
});
}
return attempt(0);
}
function toObject(arr) {
const obj = {};
for (let i = 0; i < arr.length; i += 2) {
obj[arr[i]] = arr[i + 1];
}
return obj;
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
function normalise(args) {
args = Array.from(args);
if (isObject(args[0])) {
return args[0];
}
return toObject(args);
}
function validatePlugin(plugin) {
if (typeof plugin !== 'function') throw new TypeError('Plugin is not a function');
}
module.exports = HttpTransportClient;