UNPKG

@openveo/rest-nodejs-client

Version:
250 lines (207 loc) 6.99 kB
'use strict'; /** * @module openveo-rest-nodejs-client/Request */ const timers = require('timers'); const FormData = require('form-data'); class Request { /** * Creates a REST request which can be executed / aborted. * * @class Request * @constructor * @param {String} protocol The protocol to use for the request (either 'http' or 'https') * @param {Object} [options] The complete list of http(s) options as described by NodeJS http.request * documentation. More headers can be added when executing the request. * @param {(String|Object)} [body] The request body * @param {Number} [timeout=10000] Maximum execution time for the request (in ms), set it to Infinity for a request * without limits * @param {Boolean} [multiparted=false] true to send body as multipart/form-data * @throws {TypeError} Thrown if protocol is not valid or options is not a valid object */ constructor(protocol, options, body, timeout, multiparted) { if (!options || (options && typeof options !== 'object')) throw new TypeError('Invalid request options'); if (!protocol || typeof protocol !== 'string') throw new TypeError('Invalid protocol'); if (body && typeof body !== 'string' && !multiparted) body = JSON.stringify(body); Object.defineProperties(this, /** @lends module:openveo-rest-nodejs-client/Request~Request */ { /** * Request protocol either "http" or "https". * * @type {String} * @instance * @readonly */ protocol: {value: protocol}, /** * Request options. * * @type {Object} * @instance * @readonly */ options: {value: options}, /** * The request body. * * @type {String} * @instance * @readonly */ body: {value: body}, /** * The HTTP(S) request. * * @type {Object} * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/http.html#http_class_http_clientrequest} * @instance */ request: {writable: true}, /** * Maximum execution time for the request (in ms). * * @type {Number} * @default 10000 * @instance */ executionTimeout: {value: (timeout || 10000), writable: true}, /** * Maximum time to wait until the request is aborted (in ms). * * @type {Number} * @default 2000 * @instance */ abortTimeout: {value: 2000, writable: true}, /** * Indicates if request is actually running. * * @type {Boolean} * @default false * @instance */ isRunning: {value: false, writable: true}, /** * The number of attempts made on this request. * * @type {Number} * @default 0 * @instance */ attempts: {value: 0, writable: true}, /** * Indicates if request body must be sent as multipart/form-data. * * @type {Boolean} * @default false * @instance */ multiparted: {value: multiparted, writable: true} } ); } /** * Executes the request. * * Be careful, if request is executed while still running, the running one will be aborted. * * @async * @param {Object} [headers] A list of http(s) headers. Headers will be merged with Request headers set in * the constructor. It takes priority over Request headers. * @return {Promise} Promise resolving with request's response as an Object, all request's responses are * considered success, promise is rejected only if an error occured during the transfer or while parsing the * reponse's body (expected JSON) * @throws {TypeError} Thrown if options is not a valid object */ execute(headers) { if (headers && typeof headers !== 'object') throw new TypeError('Invalid request options'); let form; this.isRunning = true; this.options.headers = Object.assign(this.options.headers, headers || {}); if (this.multiparted) { // Request body should be sent as multipart/form-data, use form-data module to achieve this form = new FormData(); this.options.headers = form.getHeaders(this.options.headers); } return this.abort().then(() => { return new Promise((resolve, reject) => { this.isRunning = true; // Send request to the web service this.request = require(this.protocol).request(this.options, (response) => { let body = ''; response.setEncoding('utf8'); response.on('error', (error) => { this.isRunning = false; return reject(error); }); response.on('data', (chunk) => body += chunk); response.on('end', () => { this.isRunning = false; try { const result = body ? JSON.parse(body) : {}; result.httpCode = response.statusCode; resolve(result); } catch (error) { reject(new Error('Server error, response is not valid JSON')); } }); }); this.request.on('error', (error) => { this.isRunning = false; return reject(error); }); if (this.executionTimeout !== Infinity) { this.request.setTimeout(this.executionTimeout, () => { this.abort().then(() => { reject(new Error('Server unavaible')); }).catch(() => { reject(new Error('Request can\'t be aborted')); }); }); } if (this.body) { if (!this.multiparted) { // Request body should be sent as is this.request.write(this.body); return this.request.end(); } // Request body should be sent as multipart/form-data for (const fieldName in this.body) { form.append(fieldName, this.body[fieldName]); } form.pipe(this.request); } else this.request.end(); }); }); } /** * Aborts the request. * * @async * @return {Promise} Promise resolving when request has been aborted, promise is rejected if it takes too long * to abort the request */ abort() { return new Promise((resolve, reject) => { if (this.request && this.isRunning) { const timeoutReference = timers.setTimeout(() => { reject('Request couldn\'t be aborted'); }, this.abortTimeout); this.request.on('abort', () => { this.isRunning = false; timers.clearTimeout(timeoutReference); resolve(); }); this.request.abort(); } else resolve(); }); } } module.exports = Request;