UNPKG

8.88 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const compose = require('koa-compose');
5const context = require('./context');
6const rejectedPromise = require('./rejectedPromise');
7const bind = require('./bind');
8
9/** Core client */
10class HttpTransportClient {
11 /**
12 * Create a HttpTransport.
13 * @param {Transport} transport - Transport instance.
14 * @param {object} defaults - default configutation
15 */
16 constructor(transport, defaults) {
17 this._transport = transport;
18 this._instancePlugins = _.get(defaults, 'plugins', []);
19 this._defaults = defaults;
20 this._initContext();
21 bind(this);
22 }
23
24 /**
25 * Registers a per request plugin
26 *
27 * @return a HttpTransport instance
28 * @param {function} fn - per request plugin
29 * @example
30 * const toError = require('@bbc/http-transport-to-error');
31 * const httpTransport = require('@bbc/http-transport');
32 *
33 * httpTransport.createClient()
34 * .use(toError(404));
35 */
36 use(plugin) {
37 validatePlugin(plugin);
38 this._ctx.addPlugin(plugin);
39 return this;
40 }
41
42 /**
43 * Make a HTTP GET request
44 *
45 * @param {string} url
46 * @return a HttpTransport instance
47 * @example
48 * const httpTransport = require('@bbc/http-transport');
49 *
50 * const response = await httpTransport.createClient()
51 * .get(url)
52 * .asResponse();
53 */
54 get(baseUrl) {
55 this._ctx.req.method('GET').baseUrl(baseUrl);
56 return this;
57 }
58
59 /**
60 * Make a HTTP POST request
61 *
62 * @param {string} url
63 * @param {object} request body
64 * @return a HttpTransport instance
65 * @example
66 * const httpTransport = require('@bbc/http-transport');
67 *
68 * const response = await httpTransport.createClient()
69 * .post(url, requestBody)
70 * .asResponse();
71 */
72 post(url, body) {
73 this._ctx.req
74 .method('POST')
75 .body(body)
76 .baseUrl(url);
77 return this;
78 }
79
80 /**
81 * Make a HTTP PUT request
82 *
83 * @param {string} url
84 * @param {object} request body
85 * @return a HttpTransport instance
86 * @example
87 * const httpTransport = require('@bbc/http-transport');
88 *
89 * const response = await httpTransport.createClient()
90 * .put(url, requestBody)
91 * .asResponse();
92 */
93 put(url, body) {
94 this._ctx.req
95 .method('PUT')
96 .body(body)
97 .baseUrl(url);
98 return this;
99 }
100
101 /**
102 * Make a HTTP DELETE request
103 *
104 * @param {string} url
105 * @param {object} request body
106 * @return a HttpTransport instance
107 * @example
108 * const httpTransport = require('@bbc/http-transport');
109 *
110 * const response = await httpTransport.createClient()
111 * .delete(url)
112 * .asResponse();
113 */
114 delete(url) {
115 this._ctx.req.method('DELETE').baseUrl(url);
116 return this;
117 }
118
119 /**
120 * Make a HTTP PATCH request
121 *
122 * @param {string} url
123 * @param {object} request body
124 * @return a HttpTransport instance
125 * @example
126 * const httpTransport = require('@bbc/http-transport');
127 *
128 * const response = await httpTransport.createClient()
129 * .put(url, requestBody)
130 * .asResponse();
131 */
132 patch(url, body) {
133 this._ctx.req
134 .method('PATCH')
135 .body(body)
136 .baseUrl(url);
137 return this;
138 }
139
140 /**
141 * Make a HTTP HEAD request
142 *
143 * @param {string} url
144 * @return a HttpTransport instance
145 * @example
146 * const httpTransport = require('@bbc/http-transport');
147 *
148 * const response = await httpTransport.createClient()
149 * .head(url)
150 * .asResponse();
151 */
152 head(url) {
153 this._ctx.req.method('HEAD').baseUrl(url);
154 return this;
155 }
156
157 /**
158 * Sets the request headers
159 *
160 * @param {string|object} name - header name or headers object
161 * @param {string|object} value - header value
162 * @return a HttpTransport instance
163 * @example
164 * const httpTransport = require('@bbc/http-transport');
165 *
166 * const response = await httpTransport.createClient()
167 * .headers({
168 * 'User-Agent' : 'someUserAgent'
169 * })
170 * .asResponse();
171 */
172 headers() {
173 const args = normalise(arguments);
174 Object.keys(args).forEach((key) => {
175 this._ctx.req.addHeader(key, args[key]);
176 });
177 return this;
178 }
179
180 /**
181 * Sets the query strings
182 *
183 * @param {string|object} name - query name or query object
184 * @param {string|object} value - query value
185 * @return a HttpTransport instance
186 * @example
187 * const httpTransport = require('@bbc/http-transport');
188 *
189 * const response = await httpTransport.createClient()
190 * .query({
191 * 'perPage' : 1
192 * })
193 * .asResponse();
194 */
195 query() {
196 const args = normalise(arguments);
197 Object.keys(args).forEach((key) => {
198 this._ctx.req.addQuery(key, args[key]);
199 });
200 return this;
201 }
202
203 /**
204 * Sets a request timeout
205 *
206 * @param {integer} timeout - timeout in seconds
207 * @return a HttpTransport instance
208 * @example
209 * const httpTransport = require('@bbc/http-transport');
210 *
211 * const response = await httpTransport.createClient()
212 * .timeout(1)
213 * .asResponse();
214 */
215 timeout(secs) {
216 this._ctx.req.timeout(secs);
217 return this;
218 }
219
220 /**
221 * Set the number of retries on failure for the request
222 *
223 * @param {integer} timeout - number of times to retry a failed request
224 * @return a HttpTransport instance
225 * @example
226 * const httpTransport = require('@bbc/http-transport');
227 *
228 * const response = await httpTransport.createClient()
229 * .retry(5) // for this request only
230 * .asResponse();
231 */
232 retry(retries) {
233 this._ctx.retries = retries;
234 return this;
235 }
236
237 /**
238 * Set the delay between retries in ms
239 *
240 * @param {integer} timeout - number of ms to wait between retries (default: 100)
241 * @return a HttpTransport instance
242 * @example
243 * const httpTransport = require('@bbc/http-transport');
244 *
245 * const response = await httpTransport.createClient()
246 * .retry(2)
247 * .retryDelay(200)
248 * .asResponse();
249 */
250 retryDelay(ms) {
251 this._ctx.retryDelay = ms;
252 return this;
253 }
254
255 /**
256 * Initiates the request, returning the response body, if successful.
257 *
258 * @return a Promise. If the Promise fulfils,
259 * the fulfilment value is the response body, as a string by default.
260 * @example
261 * const httpTransport = require('@bbc/http-transport');
262 *
263 * const body = await httpTransport.createClient()
264 * .asBody();
265 *
266 * console.log(body);
267 */
268 async asBody() {
269 const res = await this.asResponse();
270 return res.body;
271 }
272
273 /**
274 * Initiates the request, returning a http transport response object, if successful.
275 *
276 * @return a Promise. If the Promise fulfils,
277 * the fulfilment value is response object.
278 * @example
279 * const httpTransport = require('@bbc/http-transport');
280 *
281 * const response = await httpTransport.createClient()
282 * .asResponse()
283 *
284 * console.log(response);
285 *
286 */
287 async asResponse() {
288 const currentContext = this._ctx;
289 this._initContext();
290
291 const ctx = await retry(this._executeRequest, currentContext);
292 return ctx.res;
293 }
294
295 _getPlugins(ctx) {
296 return this._instancePlugins.concat(ctx.plugins);
297 }
298
299 _applyPlugins(ctx, next) {
300 const fn = compose(this._getPlugins(ctx));
301 return fn(ctx, next);
302 }
303
304 async _executeRequest(ctx) {
305 await this._applyPlugins(ctx, this._handleRequest);
306 return ctx;
307 }
308
309 async _handleRequest(ctx, next) {
310 await this._transport.execute(ctx);
311 return next();
312 }
313
314 _initContext() {
315 this._ctx = context.create(this._defaults);
316 this.headers('User-Agent', this._ctx.userAgent);
317 }
318}
319
320function isCriticalError(err) {
321 if (err && err.statusCode < 500) {
322 return false;
323 }
324 return true;
325}
326
327function toRetry(err) {
328 return {
329 reason: err.message,
330 statusCode: err.statusCode
331 };
332}
333
334function retry(fn, ctx) {
335 ctx.retryAttempts = [];
336 const maxAttempts = ctx.retries;
337
338 function attempt(i) {
339 return fn(ctx)
340 .catch((err) => {
341 if (maxAttempts > 0) {
342 const delayBy = rejectedPromise(ctx.retryDelay);
343 return delayBy(err);
344 }
345 throw err;
346 })
347 .catch((err) => {
348 if (i < maxAttempts && isCriticalError(err)) {
349 ctx.retryAttempts.push(toRetry(err));
350 return attempt(++i);
351 }
352 throw err;
353 });
354 }
355 return attempt(0);
356}
357
358function toObject(arr) {
359 const obj = {};
360 for (let i = 0; i < arr.length; i += 2) {
361 obj[arr[i]] = arr[i + 1];
362 }
363 return obj;
364}
365
366function isObject(value) {
367 return value !== null && typeof value === 'object';
368}
369
370function normalise(args) {
371 args = Array.from(args);
372 if (isObject(args[0])) {
373 return args[0];
374 }
375 return toObject(args);
376}
377
378function validatePlugin(plugin) {
379 if (typeof plugin !== 'function') throw new TypeError('Plugin is not a function');
380}
381
382module.exports = HttpTransportClient;