Source: http-api.js

  1. 'use strict';
  2. var inherits = require('inherits'),
  3. events = require('events'),
  4. _ = require('lodash/core'),
  5. Promise = require('./promise');
  6. /**
  7. * HTTP based API class with authorization hook
  8. *
  9. * @constructor
  10. * @extends events.EventEmitter
  11. * @param {Connection} conn - Connection object
  12. * @param {Object} [options] - Http API Options
  13. * @param {String} [options.responseType] - Overriding content mime-type in response
  14. * @param {Transport} [options.transport] - Transport for http api
  15. * @param {Object} [options.noContentResponse] - Alternative response when no content returned in response (= HTTP 204)
  16. */
  17. var HttpApi = function(conn, options) {
  18. options = options || {};
  19. this._conn = conn;
  20. this.on('resume', function(err) { conn.emit('resume', err); });
  21. this._responseType = options.responseType;
  22. this._transport = options.transport || conn._transport;
  23. this._noContentResponse = options.noContentResponse;
  24. };
  25. inherits(HttpApi, events.EventEmitter);
  26. /**
  27. * Callout to API endpoint using http
  28. *
  29. * @param {Object} request - Http Request object
  30. * @param {String} request.url - Endpoint URL to request
  31. * @param {String} request.method - Http method for request
  32. * @param {Object} [request.headers] - Http request headers in hash object
  33. * @param {Callback.<Object>} callback - Callback function
  34. * @returns {Promise.<Object>} -
  35. */
  36. HttpApi.prototype.request = function(request, callback) {
  37. var self = this;
  38. var conn = this._conn;
  39. var logger = conn._logger;
  40. var refreshDelegate = this.getRefreshDelegate();
  41. // remember previous instance url in case it changes after a refresh
  42. var lastInstanceUrl = conn.instanceUrl;
  43. var deferred = Promise.defer();
  44. var onResume = function(err) {
  45. if (err) {
  46. deferred.reject(err);
  47. return;
  48. }
  49. // check to see if the token refresh has changed the instance url
  50. if(lastInstanceUrl !== conn.instanceUrl){
  51. // if the instance url has changed
  52. // then replace the current request urls instance url fragment
  53. // with the updated instance url
  54. request.url = request.url.replace(lastInstanceUrl,conn.instanceUrl);
  55. }
  56. self.request(request).then(function(response) {
  57. deferred.resolve(response);
  58. }, function(err) {
  59. deferred.reject(err);
  60. });
  61. };
  62. if (refreshDelegate && refreshDelegate._refreshing) {
  63. refreshDelegate.once('resume', onResume);
  64. return deferred.promise.thenCall(callback);
  65. }
  66. // hook before sending
  67. self.beforeSend(request);
  68. self.emit('request', request);
  69. logger.debug("<request> method=" + request.method + ", url=" + request.url);
  70. var requestTime = Date.now();
  71. return this._transport.httpRequest(request).then(function(response) {
  72. var responseTime = Date.now();
  73. logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
  74. logger.debug("<response> status=" + response.statusCode + ", url=" + request.url);
  75. self.emit('response', response);
  76. // Refresh token if session has been expired and requires authentication
  77. // when session refresh delegate is available
  78. if (self.isSessionExpired(response) && refreshDelegate) {
  79. refreshDelegate.refresh(requestTime, onResume);
  80. return deferred.promise;
  81. }
  82. if (self.isErrorResponse(response)) {
  83. var err = self.getError(response);
  84. throw err;
  85. }
  86. return self.getResponseBody(response);
  87. }, function(err) {
  88. var responseTime = Date.now();
  89. logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
  90. logger.error(err);
  91. throw err;
  92. })
  93. .thenCall(callback);
  94. };
  95. /**
  96. * @protected
  97. */
  98. HttpApi.prototype.getRefreshDelegate = function() {
  99. return this._conn._refreshDelegate;
  100. };
  101. /**
  102. *
  103. * @protected
  104. */
  105. HttpApi.prototype.beforeSend = function(request) {
  106. request.headers = request.headers || {};
  107. if (this._conn.accessToken) {
  108. request.headers.Authorization = "Bearer " + this._conn.accessToken;
  109. }
  110. if (this._conn.callOptions) {
  111. var callOptions = [];
  112. for (var name in this._conn.callOptions) {
  113. callOptions.push(name + "=" + this._conn.callOptions[name]);
  114. }
  115. request.headers["Sforce-Call-Options"] = callOptions.join(', ');
  116. }
  117. };
  118. /**
  119. * Detect response content mime-type
  120. * @protected
  121. */
  122. HttpApi.prototype.getResponseContentType = function(response) {
  123. return this._responseType || response.headers && response.headers["content-type"];
  124. };
  125. /**
  126. *
  127. */
  128. HttpApi.prototype.parseResponseBody = function(response) {
  129. var contentType = this.getResponseContentType(response);
  130. var parseBody = /^(text|application)\/xml(;|$)/.test(contentType) ? parseXML :
  131. /^application\/json(;|$)/.test(contentType) ? parseJSON :
  132. /^text\/csv(;|$)/.test(contentType) ? parseCSV :
  133. parseText;
  134. try {
  135. return parseBody(response.body);
  136. } catch(e) {
  137. return response.body;
  138. }
  139. };
  140. /**
  141. * Get response body
  142. * @protected
  143. */
  144. HttpApi.prototype.getResponseBody = function(response) {
  145. if (response.statusCode === 204) { // No Content
  146. return this._noContentResponse;
  147. }
  148. var body = this.parseResponseBody(response);
  149. var err;
  150. if (this.hasErrorInResponseBody(body)) {
  151. err = this.getError(response, body);
  152. throw err;
  153. }
  154. if (response.statusCode === 300) { // Multiple Choices
  155. err = new Error('Multiple records found');
  156. err.name = "MULTIPLE_CHOICES";
  157. err.content = body;
  158. throw err;
  159. }
  160. return body;
  161. };
  162. /** @private */
  163. function parseJSON(str) {
  164. return JSON.parse(str);
  165. }
  166. /** @private */
  167. function parseXML(str) {
  168. var ret = {};
  169. require('xml2js').parseString(str, { explicitArray: false }, function(err, result) {
  170. ret = { error: err, result : result };
  171. });
  172. if (ret.error) { throw ret.error; }
  173. return ret.result;
  174. }
  175. /** @private */
  176. function parseCSV(str) {
  177. return require('./csv').parseCSV(str);
  178. }
  179. /** @private */
  180. function parseText(str) { return str; }
  181. /**
  182. * Detect session expiry
  183. * @protected
  184. */
  185. HttpApi.prototype.isSessionExpired = function(response) {
  186. return response.statusCode === 401;
  187. };
  188. /**
  189. * Detect error response
  190. * @protected
  191. */
  192. HttpApi.prototype.isErrorResponse = function(response) {
  193. return response.statusCode >= 400;
  194. };
  195. /**
  196. * Detect error in response body
  197. * @protected
  198. */
  199. HttpApi.prototype.hasErrorInResponseBody = function(body) {
  200. return false;
  201. };
  202. /**
  203. * Parsing error message in response
  204. * @protected
  205. */
  206. HttpApi.prototype.parseError = function(body) {
  207. var errors = body;
  208. return _.isArray(errors) ? errors[0] : errors;
  209. };
  210. /**
  211. * Get error message in response
  212. * @protected
  213. */
  214. HttpApi.prototype.getError = function(response, body) {
  215. var error;
  216. try {
  217. error = this.parseError(body || this.parseResponseBody(response));
  218. } catch(e) {}
  219. error = _.isObject(error) && _.isString(error.message) ? error : {
  220. errorCode: 'ERROR_HTTP_' + response.statusCode,
  221. message : response.body
  222. };
  223. var err = new Error(error.message);
  224. err.name = error.errorCode;
  225. for (var key in error) { err[key] = error[key]; }
  226. return err;
  227. };
  228. /*-------------------------------------------------------------------------*/
  229. /**
  230. * @protected
  231. */
  232. var SessionRefreshDelegate = function(conn, refreshFn) {
  233. this._conn = conn;
  234. this._refreshFn = refreshFn;
  235. this._refreshing = false;
  236. };
  237. inherits(SessionRefreshDelegate, events.EventEmitter);
  238. /**
  239. * Refresh access token
  240. * @private
  241. */
  242. SessionRefreshDelegate.prototype.refresh = function(since, callback) {
  243. // Callback immediately When refreshed after designated time
  244. if (this._lastRefreshedAt > since) { return callback(); }
  245. var self = this;
  246. var conn = this._conn;
  247. var logger = conn._logger;
  248. self.once('resume', callback);
  249. if (self._refreshing) { return; }
  250. logger.debug("<refresh token>");
  251. self._refreshing = true;
  252. return self._refreshFn(conn, function(err, accessToken, res) {
  253. if (!err) {
  254. logger.debug("Connection refresh completed. Refreshed access token = " + accessToken);
  255. conn.accessToken = accessToken;
  256. conn.emit("refresh", accessToken, res);
  257. }
  258. self._lastRefreshedAt = Date.now();
  259. self._refreshing = false;
  260. self.emit('resume', err);
  261. });
  262. };
  263. /**
  264. *
  265. */
  266. HttpApi.SessionRefreshDelegate = SessionRefreshDelegate;
  267. module.exports = HttpApi;