UNPKG

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