Completed
Push — dev ( 1671a2...d2c354 )
by Fike
31s
created

basic.js ➔ ... ➔ timeout.then   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
/* global Net */
2
3
var timeout = require('../concurrent').timeout
4
var C = require('./_common')
5
var Q = C.Query
6
var H = C.Headers
7
var Slf4j = require('../logger').Slf4j
8
9
/**
10
 * Provides client configuration defaults.
11
 *
12
 * @returns {BasicHttpClientSettings}
13
 */
14
function getDefaults () {
15
  return {
16
    url: '',
17
    retryOnNetworkError: true,
18
    throwOnServerError: true,
19
    retryOnServerError: true,
20
    throwOnClientError: true,
21
    retryOnClientError: false,
22
    throwOnNotFound: false,
23
    retryOnNotFound: false,
24
    retries: 4,
25
    logger: {}
26
  }
27
}
28
29
/**
30
 * Internal response type, used to simplify processing path finding.
31
 *
32
 * RS is an abbreviation from ResponseStatus
33
 *
34
 * @enum
35
 * @readonly
36
 */
37
var RS = {
38
  NetworkError: 'NetworkError',
39
  ServerError: 'ServerError',
40
  ClientError: 'ClientError',
41
  NotFound: 'NotFound',
42
  Ok: 'Ok',
43
  fromCode: function (code) {
44
    if (code < 200 || !code) {
45
      return RS.NetworkError
46
    }
47
    if (code < 400) {
48
      return RS.Ok
49
    }
50
    if (code === 404) {
51
      return RS.NotFound
52
    }
53
    if (code < 500) {
54
      return RS.ClientError
55
    }
56
    return RS.ServerError
57
  }
58
}
59
60
/**
61
 * @typedef {Object} BasicHttpClientSettings
62
 *
63
 * @property {string} url
64
 * @property {boolean} retryOnNetworkError
65
 * @property {boolean} throwOnServerError
66
 * @property {boolean} retryOnServerError
67
 * @property {boolean} throwOnClientError
68
 * @property {boolean} retryOnClientError
69
 * @property {boolean} throwOnNotFound
70
 * @property {boolean} retryOnNotFound
71
 * @property {string} methodOverrideHeader
72
 * @property {HeaderBag} headers Headers to be used on every request.
73
 * @property {int} retries Maximum number of retries allowed for request.
74
 * @property {LoggerOptions} logger Logger instance or context and/or name.
75
 * @property {int} timeout
76
 */
77
78
/**
79
 * @class
80
 *
81
 * @implements IHttpClient
82
 *
83
 * @param {BasicHttpClientSettings|object} [settings]
84
 * @param {netHttpRequestAsync} [transport]
85
 */
86
function BasicHttpClient (settings, transport) {
87
  transport = transport || Net.httpRequestAsync
88
  settings = settings || {}
89
  var defaults = getDefaults()
90
  var logger = Slf4j.factory(setting('logger'), 'ama-team.voxengine-sdk.http.basic')
91
  var self = this
92
  var requests = 0
93
94
  function fetch (source, key, def) {
95
    return source[key] !== undefined ? source[key] : def
96
  }
97
98
  function setting (key, def) {
99
    return fetch(settings, key, fetch(defaults, key, def))
100
  }
101
102
  function shouldRetry (status, attempt) {
103
    // number of attempts = 1 + number of retries, so it's 'greater than' rather than 'greater or equal to'
104
    // comparison
105
    return attempt <= setting('retries') && setting('retryOn' + status, false)
106
  }
107
108
  function shouldThrow (status) {
109
    return status === RS.NetworkError || setting('throwOn' + status, false)
110
  }
111
112
  function performThrow (status, request, response) {
113
    // yes, i'm counting bytes and switch is more expensive
114
    if (status === RS.ServerError) {
115
      throw new C.ServerErrorException('Server returned erroneous response', request, response)
116
    } else if (status === RS.ClientError) {
117
      throw new C.ClientErrorException('Client has performed an invalid request', request, response)
118
    } else if (status === RS.NotFound) {
119
      throw new C.NotFoundException('Requested resource hasn\'t been found', request, response)
120
    }
121
    // get exception with specified code, otherwise use default one
122
    var ErrorClass = C.codeExceptionIndex[response.code] || C.NetworkException
123
    throw new ErrorClass(null, response.code, request, response)
124
  }
125
126
  /**
127
   * Executes HTTP request.
128
   *
129
   * @param {HttpRequest} request
130
   * @return {Promise.<(HttpResponse|Error)>}
131
   */
132
  function execute (request) {
133
    var message
134
    request.timeout = typeof request.timeout === 'number' ? request.timeout : settings.timeout
135
    if (!request.method) {
136
      message = 'Request method hasn\'t been specified'
137
      return Promise.reject(new C.InvalidConfigurationException(message))
138
    }
139
    var id = request.id = request.id || ++requests
140
    request.method = request.method.toUpperCase()
141
    if (['POST', 'GET'].indexOf(request.method) === -1 && !setting('methodOverrideHeader')) {
142
      message = 'Tried to execute non-GET/POST request without specifying methodOverrideHeader in settings'
143
      return Promise.reject(new C.InvalidConfigurationException(message))
144
    }
145
    request.query = Q.normalize(request.query)
146
    request.headers = H.normalize(request.headers)
147
    if (!request.payload) {
148
      request.payload = null
149
    }
150
    var url = request.url = setting('url') + (request.url || '')
151
    logger.debug('Executing request #{} `{} {}`', request.id, request.method, url)
152
    return executionLoop(request, 1)
153
      .then(function (response) {
154
        logger.debug('Request #{} `{} {}` got response with code {}', id,
155
          request.method, url, response.code)
156
        return response
157
      }, function (e) {
158
        logger.debug('Request #{} `{} {}` resulted in error {}', id,
159
          request.method, url, e.name)
160
        throw e
161
      })
162
  }
163
164
  function executionLoop (request, attempt) {
165
    var qs = Q.encode(request.query)
166
    var url = request.url + (qs.length > 0 ? '?' + qs : '')
167
    var opts = new Net.HttpRequestOptions()
168
    var method = ['HEAD', 'GET'].indexOf(request.method) === -1 ? 'POST' : 'GET'
169
    var headers = H.override(setting('headers', {}), request.headers)
170
    if (method !== request.method) {
171
      headers[setting('methodOverrideHeader')] = [request.method]
172
    }
173
    opts.method = method
174
    opts.postData = request.payload
175
    opts.headers = H.encode(headers)
176
    logger.trace('Executing request #{} `{} {}`, attempt #{}', request.id, request.method, url, attempt)
177
    return timeout(transport(url, opts), request.timeout).then(function (raw) {
178
      var response = {code: raw.code, headers: H.decode(raw.headers), payload: raw.text}
179
      var status = RS.fromCode(response.code)
180
      var toRetry = shouldRetry(status, attempt)
181
      var toThrow = !toRetry && shouldThrow(status)
182
      logger.trace('Request #{} `{} {}` (attempt #{}) ended with code `{}` / status `{}`, (retry: {}, throw: {})',
183
        request.id, request.method, url, attempt, response.code, status, toRetry, toThrow)
184
      if (toRetry) {
185
        return executionLoop(request, attempt + 1)
186
      }
187
      if (toThrow) {
188
        performThrow(status, request, response)
189
      }
190
      response.request = request
191
      return response
192
    })
193
  }
194
195
  function request (method, url, query, payload, headers, timeout) {
196
    return execute({url: url, method: method, headers: headers, query: query, payload: payload, timeout: timeout})
197
  }
198
199
  // noinspection JSUnusedGlobalSymbols
200
  this.execute = execute
201
  this.request = request
202
203
  var methods = ['get', 'head']
204
  methods.forEach(function (method) {
205
    self[method] = function (url, query, headers, timeout) {
206
      return request(method, url, query, null, headers, timeout)
207
    }
208
  })
209
  methods = ['post', 'put', 'patch', 'delete']
210
  methods.forEach(function (method) {
211
    self[method] = function (url, payload, headers, query, timeout) {
212
      return request(method, url, query, payload, headers, timeout)
213
    }
214
  })
215
216
  /**
217
   * Perform GET request.
218
   *
219
   * @function BasicHttpClient#get
220
   *
221
   * @param {string} url
222
   * @param {QueryBag} [query]
223
   * @param {HeaderBag} [headers]
224
   * @param {int} [timeout]
225
   *
226
   * @return {HttpResponsePromise}
227
   */
228
229
  /**
230
   * Perform HEAD request.
231
   *
232
   * @function BasicHttpClient#head
233
   *
234
   * @param {string} url
235
   * @param {QueryBag} [query]
236
   * @param {HeaderBag} [headers]
237
   * @param {int} [timeout]
238
   *
239
   * @return {HttpResponsePromise}
240
   */
241
242
  /**
243
   * Perform POST request.
244
   *
245
   * @function BasicHttpClient#post
246
   *
247
   * @param {string} url
248
   * @param {string} [payload]
249
   * @param {HeaderBag} [headers]
250
   * @param {int} [timeout]
251
   *
252
   * @return {HttpResponsePromise}
253
   */
254
255
  /**
256
   * Perform PUT request.
257
   *
258
   * @function BasicHttpClient#put
259
   *
260
   * @param {string} url
261
   * @param {string} [payload]
262
   * @param {HeaderBag} [headers]
263
   * @param {int} [timeout]
264
   *
265
   * @return {HttpResponsePromise}
266
   */
267
268
  /**
269
   * Perform PATCH request.
270
   *
271
   * @function BasicHttpClient#patch
272
   *
273
   * @param {string} url
274
   * @param {string} [payload]
275
   * @param {HeaderBag} [headers]
276
   * @param {int} [timeout]
277
   *
278
   * @return {HttpResponsePromise}
279
   */
280
281
  /**
282
   * Perform DELETE request.
283
   *
284
   * @function BasicHttpClient#delete
285
   *
286
   * @param {string} url
287
   * @param {string} [payload]
288
   * @param {HeaderBag} [headers]
289
   * @param {int} [timeout]
290
   *
291
   * @return {HttpResponsePromise}
292
   */
293
}
294
295
BasicHttpClient.getDefaults = getDefaults
296
297
module.exports = {
298
  Client: BasicHttpClient,
299
  /** @deprecated */
300
  getDefaults: getDefaults
301
}
302