Completed
Push — dev ( b218b7...d30456 )
by Fike
32s
created

RS.fromCode   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 1
dl 0
loc 15
rs 8.8571
c 0
b 0
f 0
1
/* global Net */
2
3
var C = require('./_common')
4
var Q = C.Query
5
var H = C.Headers
6
var Slf4j = require('../logger').slf4j.Slf4j
7
8
/**
9
 * Provides client configuration defaults.
10
 *
11
 * @returns {BasicHttpClientSettings}
12
 */
13
function getDefaults () {
14
  return {
15
    url: '',
16
    retryOnNetworkError: true,
17
    throwOnServerError: true,
18
    retryOnServerError: true,
19
    throwOnClientError: true,
20
    retryOnClientError: false,
21
    throwOnNotFound: false,
22
    retryOnNotFound: false,
23
    retries: 4,
24
    logger: {}
25
  }
26
}
27
28
/**
29
 * Internal response type, used to simplify processing path finding.
30
 *
31
 * RS is an abbreviation from ResponseStatus
32
 *
33
 * @enum
34
 * @readonly
35
 */
36
var RS = {
37
  NetworkError: 'NetworkError',
38
  ServerError: 'ServerError',
39
  ClientError: 'ClientError',
40
  NotFound: 'NotFound',
41
  Ok: 'Ok',
42
  fromCode: function (code) {
43
    if (code < 200 || !code) {
44
      return this.NetworkError
45
    }
46
    if (code < 400) {
47
      return this.Ok
48
    }
49
    if (code === 404) {
50
      return this.NotFound
51
    }
52
    if (code < 500) {
53
      return this.ClientError
54
    }
55
    return this.ServerError
56
  }
57
}
58
59
/**
60
 * @typedef {Object} BasicHttpClientSettings
61
 *
62
 * @property {string} url
63
 * @property {boolean} retryOnNetworkError
64
 * @property {boolean} throwOnServerError
65
 * @property {boolean} retryOnServerError
66
 * @property {boolean} throwOnClientError
67
 * @property {boolean} retryOnClientError
68
 * @property {boolean} throwOnNotFound
69
 * @property {boolean} retryOnNotFound
70
 * @property {string} methodOverrideHeader
71
 * @property {HeaderBag} headers Headers to be used on every request.
72
 * @property {int} retries Maximum number of retries allowed for request.
73
 * @property {LoggerOptions} logger Logger instance or context and/or name.
74
 */
75
76
/**
77
 * @class
78
 *
79
 * @implements IHttpClient
80
 *
81
 * @param {BasicHttpClientSettings|object} [settings]
82
 * @param {netHttpRequestAsync} [transport]
83
 */
84
function BasicHttpClient (settings, transport) {
85
  transport = transport || Net.httpRequestAsync
86
  settings = settings || {}
87
  var defaults = getDefaults()
88
  var logger = Slf4j.factory(setting('logger'), 'ama-team.voxengine-sdk.http.basic')
89
  var self = this
90
  var requests = 0
91
92
  function fetch (source, key, def) {
93
    return source[key] !== undefined ? source[key] : def
94
  }
95
96
  function setting (key, def) {
97
    return fetch(settings, key, fetch(defaults, key, def))
98
  }
99
100
  function shouldRetry (status, attempt) {
101
    // number of attempts = 1 + number of retries, so it's 'greater than' rather than 'greater or equal to'
102
    // comparison
103
    return attempt <= setting('retries') && setting('retryOn' + status, false)
104
  }
105
106
  function shouldThrow (status) {
107
    return status === RS.NetworkError || setting('throwOn' + status, false)
108
  }
109
110
  function performThrow (status, request, response) {
111
    // yes, i'm counting bytes and switch is more expensive
112
    if (status === RS.ServerError) {
113
      throw new C.ServerErrorException('Server returned erroneous response', request, response)
114
    } else if (status === RS.ClientError) {
115
      throw new C.ClientErrorException('Client has performed an invalid request', request, response)
116
    } else if (status === RS.NotFound) {
117
      throw new C.NotFoundException('Requested resource hasn\'t been found', request, response)
118
    }
119
    // get exception with specified code, otherwise use default one
120
    var ErrorClass = C.codeExceptionIndex[response.code] || C.NetworkException
121
    throw new ErrorClass(null, response.code, request, response)
122
  }
123
124
  /**
125
   * Executes HTTP request.
126
   *
127
   * @param {HttpRequest} request
128
   * @return {Promise.<(HttpResponse|Error)>}
129
   */
130
  function execute (request) {
131
    var message
132
    if (!request.method) {
133
      message = 'Request method hasn\'t been specified'
134
      return Promise.reject(new C.InvalidConfigurationException(message))
135
    }
136
    var id = request.id = request.id || ++requests
137
    request.method = request.method.toUpperCase()
138
    if (['POST', 'GET'].indexOf(request.method) === -1 && !setting('methodOverrideHeader')) {
139
      message = 'Tried to execute non-GET/POST request without specifying methodOverrideHeader in settings'
140
      return Promise.reject(new C.InvalidConfigurationException(message))
141
    }
142
    request.query = Q.normalize(request.query)
143
    request.headers = H.normalize(request.headers)
144
    if (!request.payload) request.payload = null
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
145
    var url = request.url = setting('url') + (request.url || '')
146
    logger.debug('Executing request #{} `{} {}`', request.id, request.method, url)
147
    return executionLoop(request, 1)
148
      .then(function (response) {
149
        logger.debug('Request #{} `{} {}` got response with code {}', id,
150
          request.method, url, response.code)
151
        return response
152
      }, function (e) {
153
        logger.debug('Request #{} `{} {}` resulted in error {}', id,
154
          request.method, url, e.name)
155
        throw e
156
      })
157
  }
158
159
  function executionLoop (request, attempt) {
160
    var qs = Q.encode(request.query)
161
    var url = request.url + (qs.length > 0 ? '?' + qs : '')
162
    var opts = new Net.HttpRequestOptions()
163
    var method = ['HEAD', 'GET'].indexOf(request.method) === -1 ? 'POST' : 'GET'
164
    var headers = H.override(setting('headers', {}), request.headers)
165
    if (method !== request.method) {
166
      headers[setting('methodOverrideHeader')] = [request.method]
167
    }
168
    opts.method = method
169
    opts.postData = request.payload
170
    opts.headers = H.encode(headers)
171
    logger.trace('Executing request #{} `{} {}`, attempt #{}', request.id, request.method, url, attempt)
172
    return transport(url, opts).then(function (raw) {
173
      var response = {code: raw.code, headers: H.decode(raw.headers), payload: raw.text}
174
      var status = RS.fromCode(response.code)
175
      var toRetry = shouldRetry(status, attempt)
176
      var toThrow = !toRetry && shouldThrow(status)
177
      logger.trace('Request #{} `{} {}` (attempt #{}) ended with code `{}` / status `{}`, (retry: {}, throw: {})',
178
        request.id, request.method, url, attempt, response.code, status, toRetry, toThrow)
179
      if (toRetry) {
180
        return executionLoop(request, attempt + 1)
181
      }
182
      if (toThrow) {
183
        performThrow(status, request, response)
184
      }
185
      response.request = request
186
      return response
187
    })
188
  }
189
190
  function request (method, url, query, payload, headers) {
191
    return execute({url: url, method: method, headers: headers, query: query, payload: payload})
192
  }
193
194
  // noinspection JSUnusedGlobalSymbols
195
  this.execute = execute
196
  this.request = request
197
198
  var methods = ['get', 'head']
199
  methods.forEach(function (method) {
200
    self[method] = function (url, query, headers) {
201
      return request(method, url, query, null, headers)
202
    }
203
  })
204
  methods = ['post', 'put', 'patch', 'delete']
205
  methods.forEach(function (method) {
206
    self[method] = function (url, payload, headers, query) {
207
      return request(method, url, query, payload, headers)
208
    }
209
  })
210
211
  /**
212
   * Perform GET request.
213
   *
214
   * @function BasicHttpClient#get
215
   *
216
   * @param {string} url
217
   * @param {QueryBag} [query]
218
   * @param {HeaderBag} [headers]
219
   *
220
   * @return {HttpResponsePromise}
221
   */
222
223
  /**
224
   * Perform HEAD request.
225
   *
226
   * @function BasicHttpClient#head
227
   *
228
   * @param {string} url
229
   * @param {QueryBag} [query]
230
   * @param {HeaderBag} [headers]
231
   *
232
   * @return {HttpResponsePromise}
233
   */
234
235
  /**
236
   * Perform POST request.
237
   *
238
   * @function BasicHttpClient#post
239
   *
240
   * @param {string} url
241
   * @param {string} [payload]
242
   * @param {HeaderBag} [headers]
243
   *
244
   * @return {HttpResponsePromise}
245
   */
246
247
  /**
248
   * Perform PUT request.
249
   *
250
   * @function BasicHttpClient#put
251
   *
252
   * @param {string} url
253
   * @param {string} [payload]
254
   * @param {HeaderBag} [headers]
255
   *
256
   * @return {HttpResponsePromise}
257
   */
258
259
  /**
260
   * Perform PATCH request.
261
   *
262
   * @function BasicHttpClient#patch
263
   *
264
   * @param {string} url
265
   * @param {string} [payload]
266
   * @param {HeaderBag} [headers]
267
   *
268
   * @return {HttpResponsePromise}
269
   */
270
271
  /**
272
   * Perform DELETE request.
273
   *
274
   * @function BasicHttpClient#delete
275
   *
276
   * @param {string} url
277
   * @param {string} [payload]
278
   * @param {HeaderBag} [headers]
279
   *
280
   * @return {HttpResponsePromise}
281
   */
282
}
283
284
BasicHttpClient.getDefaults = getDefaults
285
286
module.exports = {
287
  Client: BasicHttpClient,
288
  /** @deprecated */
289
  getDefaults: getDefaults
290
}
291