Source: Request.js

  1. 'use strict';
  2. /**
  3. * @module openveo-rest-nodejs-client/Request
  4. */
  5. const timers = require('timers');
  6. const FormData = require('form-data');
  7. class Request {
  8. /**
  9. * Creates a REST request which can be executed / aborted.
  10. *
  11. * @class Request
  12. * @constructor
  13. * @param {String} protocol The protocol to use for the request (either 'http' or 'https')
  14. * @param {Object} [options] The complete list of http(s) options as described by NodeJS http.request
  15. * documentation. More headers can be added when executing the request.
  16. * @param {(String|Object)} [body] The request body
  17. * @param {Number} [timeout=10000] Maximum execution time for the request (in ms), set it to Infinity for a request
  18. * without limits
  19. * @param {Boolean} [multiparted=false] true to send body as multipart/form-data
  20. * @throws {TypeError} Thrown if protocol is not valid or options is not a valid object
  21. */
  22. constructor(protocol, options, body, timeout, multiparted) {
  23. if (!options || (options && typeof options !== 'object'))
  24. throw new TypeError('Invalid request options');
  25. if (!protocol || typeof protocol !== 'string')
  26. throw new TypeError('Invalid protocol');
  27. if (body && typeof body !== 'string' && !multiparted)
  28. body = JSON.stringify(body);
  29. Object.defineProperties(this,
  30. /** @lends module:openveo-rest-nodejs-client/Request~Request */
  31. {
  32. /**
  33. * Request protocol either "http" or "https".
  34. *
  35. * @type {String}
  36. * @instance
  37. * @readonly
  38. */
  39. protocol: {value: protocol},
  40. /**
  41. * Request options.
  42. *
  43. * @type {Object}
  44. * @instance
  45. * @readonly
  46. */
  47. options: {value: options},
  48. /**
  49. * The request body.
  50. *
  51. * @type {String}
  52. * @instance
  53. * @readonly
  54. */
  55. body: {value: body},
  56. /**
  57. * The HTTP(S) request.
  58. *
  59. * @type {Object}
  60. * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/http.html#http_class_http_clientrequest}
  61. * @instance
  62. */
  63. request: {writable: true},
  64. /**
  65. * Maximum execution time for the request (in ms).
  66. *
  67. * @type {Number}
  68. * @default 10000
  69. * @instance
  70. */
  71. executionTimeout: {value: (timeout || 10000), writable: true},
  72. /**
  73. * Maximum time to wait until the request is aborted (in ms).
  74. *
  75. * @type {Number}
  76. * @default 2000
  77. * @instance
  78. */
  79. abortTimeout: {value: 2000, writable: true},
  80. /**
  81. * Indicates if request is actually running.
  82. *
  83. * @type {Boolean}
  84. * @default false
  85. * @instance
  86. */
  87. isRunning: {value: false, writable: true},
  88. /**
  89. * The number of attempts made on this request.
  90. *
  91. * @type {Number}
  92. * @default 0
  93. * @instance
  94. */
  95. attempts: {value: 0, writable: true},
  96. /**
  97. * Indicates if request body must be sent as multipart/form-data.
  98. *
  99. * @type {Boolean}
  100. * @default false
  101. * @instance
  102. */
  103. multiparted: {value: multiparted, writable: true}
  104. }
  105. );
  106. }
  107. /**
  108. * Executes the request.
  109. *
  110. * Be careful, if request is executed while still running, the running one will be aborted.
  111. *
  112. * @async
  113. * @param {Object} [headers] A list of http(s) headers. Headers will be merged with Request headers set in
  114. * the constructor. It takes priority over Request headers.
  115. * @return {Promise} Promise resolving with request's response as an Object, all request's responses are
  116. * considered success, promise is rejected only if an error occured during the transfer or while parsing the
  117. * reponse's body (expected JSON)
  118. * @throws {TypeError} Thrown if options is not a valid object
  119. */
  120. execute(headers) {
  121. if (headers && typeof headers !== 'object')
  122. throw new TypeError('Invalid request options');
  123. let form;
  124. this.isRunning = true;
  125. this.options.headers = Object.assign(this.options.headers, headers || {});
  126. if (this.multiparted) {
  127. // Request body should be sent as multipart/form-data, use form-data module to achieve this
  128. form = new FormData();
  129. this.options.headers = form.getHeaders(this.options.headers);
  130. }
  131. return this.abort().then(() => {
  132. return new Promise((resolve, reject) => {
  133. this.isRunning = true;
  134. // Send request to the web service
  135. this.request = require(this.protocol).request(this.options, (response) => {
  136. let body = '';
  137. response.setEncoding('utf8');
  138. response.on('error', (error) => {
  139. this.isRunning = false;
  140. return reject(error);
  141. });
  142. response.on('data', (chunk) => body += chunk);
  143. response.on('end', () => {
  144. this.isRunning = false;
  145. try {
  146. const result = body ? JSON.parse(body) : {};
  147. result.httpCode = response.statusCode;
  148. resolve(result);
  149. } catch (error) {
  150. reject(new Error('Server error, response is not valid JSON'));
  151. }
  152. });
  153. });
  154. this.request.on('error', (error) => {
  155. this.isRunning = false;
  156. return reject(error);
  157. });
  158. if (this.executionTimeout !== Infinity) {
  159. this.request.setTimeout(this.executionTimeout, () => {
  160. this.abort().then(() => {
  161. reject(new Error('Server unavaible'));
  162. }).catch(() => {
  163. reject(new Error('Request can\'t be aborted'));
  164. });
  165. });
  166. }
  167. if (this.body) {
  168. if (!this.multiparted) {
  169. // Request body should be sent as is
  170. this.request.write(this.body);
  171. return this.request.end();
  172. }
  173. // Request body should be sent as multipart/form-data
  174. for (const fieldName in this.body) {
  175. form.append(fieldName, this.body[fieldName]);
  176. }
  177. form.pipe(this.request);
  178. } else
  179. this.request.end();
  180. });
  181. });
  182. }
  183. /**
  184. * Aborts the request.
  185. *
  186. * @async
  187. * @return {Promise} Promise resolving when request has been aborted, promise is rejected if it takes too long
  188. * to abort the request
  189. */
  190. abort() {
  191. return new Promise((resolve, reject) => {
  192. if (this.request && this.isRunning) {
  193. const timeoutReference = timers.setTimeout(() => {
  194. reject('Request couldn\'t be aborted');
  195. }, this.abortTimeout);
  196. this.request.on('abort', () => {
  197. this.isRunning = false;
  198. timers.clearTimeout(timeoutReference);
  199. resolve();
  200. });
  201. this.request.abort();
  202. } else
  203. resolve();
  204. });
  205. }
  206. }
  207. module.exports = Request;