1 | ;
|
2 |
|
3 | const _ = require('lodash');
|
4 | const compose = require('koa-compose');
|
5 | const context = require('./context');
|
6 | const rejectedPromise = require('./rejectedPromise');
|
7 | const bind = require('./bind');
|
8 |
|
9 | /** Core client */
|
10 | class 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 |
|
320 | function isCriticalError(err) {
|
321 | if (err && err.statusCode < 500) {
|
322 | return false;
|
323 | }
|
324 | return true;
|
325 | }
|
326 |
|
327 | function toRetry(err) {
|
328 | return {
|
329 | reason: err.message,
|
330 | statusCode: err.statusCode
|
331 | };
|
332 | }
|
333 |
|
334 | function 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 |
|
358 | function 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 |
|
366 | function isObject(value) {
|
367 | return value !== null && typeof value === 'object';
|
368 | }
|
369 |
|
370 | function normalise(args) {
|
371 | args = Array.from(args);
|
372 | if (isObject(args[0])) {
|
373 | return args[0];
|
374 | }
|
375 | return toObject(args);
|
376 | }
|
377 |
|
378 | function validatePlugin(plugin) {
|
379 | if (typeof plugin !== 'function') throw new TypeError('Plugin is not a function');
|
380 | }
|
381 |
|
382 | module.exports = HttpTransportClient;
|