request.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. "use strict";
  2. var _ = require('../../lodash'), PropertyBase = require('./property-base').PropertyBase, Property = require('./property').Property, Url = require('./url').Url,
  3. // ProxyConfig = require('./proxy-config').ProxyConfig,
  4. // Certificate = require('./certificate').Certificate,
  5. HeaderList = require('./header-list').HeaderList, RequestBody = require('./request-body').RequestBody,
  6. // RequestAuth = require('./request-auth').RequestAuth,
  7. Request,
  8. /**
  9. * Default request method
  10. *
  11. * @private
  12. * @const
  13. * @type {String}
  14. */
  15. DEFAULT_REQ_METHOD = 'GET',
  16. /**
  17. * Content length header name
  18. *
  19. * @private
  20. * @const
  21. * @type {String}
  22. */
  23. CONTENT_LENGTH = 'Content-Length',
  24. /**
  25. * Single space
  26. *
  27. * @private
  28. * @const
  29. * @type {String}
  30. */
  31. SP = ' ',
  32. /**
  33. * Carriage return + line feed
  34. *
  35. * @private
  36. * @const
  37. * @type {String}
  38. */
  39. CRLF = '\r\n',
  40. /**
  41. * HTTP version
  42. *
  43. * @private
  44. * @const
  45. * @type {String}
  46. */
  47. HTTP_X_X = 'HTTP/X.X',
  48. /**
  49. * @private
  50. * @type {Boolean}
  51. */
  52. supportsBuffer = (typeof Buffer !== undefined) && _.isFunction(Buffer.byteLength),
  53. /**
  54. * Source of request body size calculation.
  55. * Either computed from body or used Content-Length header value.
  56. *
  57. * @private
  58. * @const
  59. * @type {Object}
  60. */
  61. SIZE_SOURCE = {
  62. computed: 'COMPUTED',
  63. contentLength: 'CONTENT-LENGTH'
  64. };
  65. /**
  66. * @typedef Request.definition
  67. * @property {String|Url} url The URL of the request. This can be a {@link Url.definition} or a string.
  68. * @property {String} method The request method, e.g: "GET" or "POST".
  69. * @property {Array<Header.definition>} header The headers that should be sent as a part of this request.
  70. * @property {RequestBody.definition} body The request body definition.
  71. * @property {RequestAuth.definition} auth The authentication/signing information for this request.
  72. * @property {ProxyConfig.definition} proxy The proxy information for this request.
  73. * @property {Certificate.definition} certificate The certificate information for this request.
  74. */
  75. _.inherit((
  76. /**
  77. * A Postman HTTP request object.
  78. *
  79. * @constructor
  80. * @extends {Property}
  81. * @param {Request.definition} options
  82. */
  83. Request = function PostmanRequest(options) {
  84. // this constructor is intended to inherit and as such the super constructor is required to be executed
  85. Request.super_.apply(this, arguments);
  86. // if the definition is a string, it implies that this is a get of URL
  87. (typeof options === 'string') && (options = {
  88. url: options
  89. });
  90. // Create the default properties
  91. _.assign(this, /** @lends Request.prototype */ {
  92. /**
  93. * @type {Url}
  94. */
  95. url: new Url(),
  96. /**
  97. * @type {HeaderList}
  98. */
  99. headers: new HeaderList(this, options && options.header),
  100. // Although a similar check is being done in the .update call below, this handles falsy options as well.
  101. /**
  102. * @type {String}
  103. * @todo: Clean this up
  104. */
  105. // the negated condition is required to keep DEFAULT_REQ_METHOD as a fallback
  106. method: _.has(options, 'method') && !_.isNil(options.method) ?
  107. String(options.method).toUpperCase() : DEFAULT_REQ_METHOD
  108. });
  109. this.update(options);
  110. }), Property);
  111. _.assign(Request.prototype, /** @lends Request.prototype */ {
  112. /**
  113. * Updates the different properties of the request.
  114. *
  115. * @param {Request.definition} options
  116. */
  117. update: function (options) {
  118. // Nothing to do
  119. if (!options) {
  120. return;
  121. }
  122. // The existing url is updated.
  123. _.has(options, 'url') && this.url.update(options.url);
  124. // The existing list of headers must be cleared before adding the given headers to it.
  125. options.header && this.headers.repopulate(options.header);
  126. // Only update the method if one is provided.
  127. _.has(options, 'method') && (this.method = _.isNil(options.method) ?
  128. DEFAULT_REQ_METHOD : String(options.method).toUpperCase());
  129. // The rest of the properties are not assumed to exist so we merge in the defined ones.
  130. _.mergeDefined(this, /** @lends Request.prototype */ {
  131. /**
  132. * @type {RequestBody|undefined}
  133. */
  134. body: _.createDefined(options, 'body', RequestBody),
  135. });
  136. },
  137. /**
  138. * Returns an object where the key is a header name and value is the header value.
  139. *
  140. * @param {Object=} options
  141. * @param {Boolean} options.ignoreCase When set to "true", will ensure that all the header keys are lower case.
  142. * @param {Boolean} options.enabled Only get the enabled headers
  143. * @param {Boolean} options.multiValue When set to "true", duplicate header values will be stored in an array
  144. * @param {Boolean} options.sanitizeKeys When set to "true", headers with falsy keys are removed
  145. * @returns {Object}
  146. * @note If multiple headers are present in the same collection with same name, but different case
  147. * (E.g "x-forward-port" and "X-Forward-Port", and `options.ignoreCase` is set to true,
  148. * the values will be stored in an array.
  149. */
  150. getHeaders: function getHeaders(options) {
  151. !options && (options = {});
  152. // @note: options.multiValue will not be respected since, Header._postman_propertyAllowsMultipleValues
  153. // gets higher precedence in PropertyLists.toObject.
  154. // @todo: sanitizeKeys for headers by default.
  155. return this.headers.toObject(options.enabled, !options.ignoreCase, options.multiValue, options.sanitizeKeys);
  156. },
  157. /**
  158. * Calls the given callback on each Header object contained within the request.
  159. *
  160. * @param {Function} callback
  161. */
  162. forEachHeader: function forEachHeader(callback) {
  163. this.headers.all().forEach(function (header) {
  164. return callback(header, this);
  165. }, this);
  166. },
  167. /**
  168. * Adds a header to the PropertyList of headers.
  169. *
  170. * @param {Header| {key: String, value: String}} header Can be a {Header} object, or a raw header object.
  171. */
  172. addHeader: function (header) {
  173. this.headers.add(header);
  174. },
  175. /**
  176. * Removes a header from the request.
  177. *
  178. * @param {String|Header} toRemove A header object to remove, or a string containing the header key.
  179. * @param {Object} options
  180. * @param {Boolean} options.ignoreCase If set to true, ignores case while removing the header.
  181. */
  182. removeHeader: function (toRemove, options) {
  183. toRemove = _.isString(toRemove) ? toRemove : toRemove.key;
  184. options = options || {};
  185. if (!toRemove) { // Nothing to remove :(
  186. return;
  187. }
  188. options.ignoreCase && (toRemove = toRemove.toLowerCase());
  189. this.headers.remove(function (header) {
  190. var key = options.ignoreCase ? header.key.toLowerCase() : header.key;
  191. return key === toRemove;
  192. });
  193. },
  194. /**
  195. * Updates or inserts the given header.
  196. *
  197. * @param {Object} header
  198. */
  199. upsertHeader: function (header) {
  200. if (!(header && header.key)) {
  201. return;
  202. } // if no valid header is provided, do nothing
  203. var existing = this.headers.find({ key: header.key });
  204. if (!existing) {
  205. return this.headers.add(header);
  206. }
  207. existing.value = header.value;
  208. },
  209. /**
  210. * Add query parameters to the request.
  211. *
  212. * @todo: Rename this?
  213. * @param {Array<QueryParam>|String} params
  214. */
  215. addQueryParams: function (params) {
  216. this.url.addQueryParams(params);
  217. },
  218. /**
  219. * Removes parameters passed in params.
  220. *
  221. * @param {String|Array} params
  222. */
  223. removeQueryParams: function (params) {
  224. this.url.removeQueryParams(params);
  225. },
  226. /**
  227. * Get the request size by computing the headers and body or using the
  228. * actual content length header once the request is sent.
  229. *
  230. * @returns {Object}
  231. */
  232. size: function () {
  233. var contentLength = this.headers.get(CONTENT_LENGTH), requestTarget = this.url.getPathWithQuery(), bodyString, sizeInfo = {
  234. body: 0,
  235. header: 0,
  236. total: 0,
  237. source: SIZE_SOURCE.computed
  238. };
  239. // if 'Content-Length' header is present, we take body as declared by
  240. // the client(postman-request or user-defined). else we need to compute the same.
  241. if (contentLength && _.isNumeric(contentLength)) {
  242. sizeInfo.body = parseInt(contentLength, 10);
  243. sizeInfo.source = SIZE_SOURCE.contentLength;
  244. }
  245. // otherwise, if body is defined, we calculate the length of the body
  246. else if (this.body) {
  247. // @note body.toString() returns E for formdata or file mode
  248. bodyString = this.body.toString();
  249. sizeInfo.body = supportsBuffer ? Buffer.byteLength(bodyString) : bodyString.length;
  250. }
  251. // https://tools.ietf.org/html/rfc7230#section-3
  252. // HTTP-message = start-line (request-line / status-line)
  253. // *( header-field CRLF )
  254. // CRLF
  255. // [ message-body ]
  256. // request-line = method SP request-target SP HTTP-version CRLF
  257. sizeInfo.header = (this.method + SP + requestTarget + SP + HTTP_X_X + CRLF + CRLF).length +
  258. this.headers.contentSize();
  259. // compute the approximate total body size by adding size of header and body
  260. sizeInfo.total = (sizeInfo.body || 0) + (sizeInfo.header || 0);
  261. return sizeInfo;
  262. },
  263. /**
  264. * Converts the Request to a plain JavaScript object, which is also how the request is
  265. * represented in a collection file.
  266. *
  267. * @returns {{url: (*|string), method: *, header: (undefined|*), body: *, auth: *, certificate: *}}
  268. */
  269. toJSON: function () {
  270. var obj = PropertyBase.toJSON(this);
  271. // remove header array if blank
  272. if (_.isArray(obj.header) && !obj.header.length) {
  273. delete obj.header;
  274. }
  275. return obj;
  276. },
  277. /**
  278. * Creates a clone of this request
  279. *
  280. * @returns {Request}
  281. */
  282. clone: function () {
  283. return new Request(this.toJSON());
  284. },
  285. /**
  286. * Creates a copy of this request, with the appropriate auth headers or parameters added.
  287. *
  288. * @deprecated discontinued in v3.x
  289. * @note This function does not take care of resolving variables.
  290. * @returns {Request}
  291. */
  292. authorize: function () {
  293. throw new Error('collection request.authorize() has been discontinued');
  294. }
  295. });
  296. _.assign(Request, /** @lends Request */ {
  297. /**
  298. * Defines the name of this property for internal use.
  299. * @private
  300. * @readOnly
  301. * @type {String}
  302. */
  303. _postman_propertyName: 'Request',
  304. /**
  305. * Check whether an object is an instance of {@link ItemGroup}.
  306. *
  307. * @param {*} obj
  308. * @returns {Boolean}
  309. */
  310. isRequest: function (obj) {
  311. return Boolean(obj) && ((obj instanceof Request) ||
  312. _.inSuperChain(obj.constructor, '_postman_propertyName', Request._postman_propertyName));
  313. }
  314. });
  315. module.exports = {
  316. Request: Request
  317. };