xhr.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. // Copyright 2011 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Offered as an alternative to XhrIo as a way for making requests
  16. * via XMLHttpRequest. Instead of mirroring the XHR interface and exposing
  17. * events, results are used as a way to pass a "promise" of the response to
  18. * interested parties.
  19. *
  20. */
  21. goog.provide('goog.labs.net.xhr');
  22. goog.provide('goog.labs.net.xhr.Error');
  23. goog.provide('goog.labs.net.xhr.HttpError');
  24. goog.provide('goog.labs.net.xhr.Options');
  25. goog.provide('goog.labs.net.xhr.PostData');
  26. goog.provide('goog.labs.net.xhr.ResponseType');
  27. goog.provide('goog.labs.net.xhr.TimeoutError');
  28. goog.require('goog.Promise');
  29. goog.require('goog.asserts');
  30. goog.require('goog.debug.Error');
  31. goog.require('goog.json');
  32. goog.require('goog.net.HttpStatus');
  33. goog.require('goog.net.XmlHttp');
  34. goog.require('goog.object');
  35. goog.require('goog.string');
  36. goog.require('goog.uri.utils');
  37. goog.require('goog.userAgent');
  38. goog.scope(function() {
  39. var userAgent = goog.userAgent;
  40. var xhr = goog.labs.net.xhr;
  41. var HttpStatus = goog.net.HttpStatus;
  42. /**
  43. * Configuration options for an XMLHttpRequest.
  44. * - headers: map of header key/value pairs.
  45. * - timeoutMs: number of milliseconds after which the request will be timed
  46. * out by the client. Default is to allow the browser to handle timeouts.
  47. * - withCredentials: whether user credentials are to be included in a
  48. * cross-origin request. See:
  49. * http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
  50. * - mimeType: allows the caller to override the content-type and charset for
  51. * the request. See:
  52. * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
  53. * - responseType: may be set to change the response type to an arraybuffer or
  54. * blob for downloading binary data. See:
  55. * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype]
  56. * - xmlHttpFactory: allows the caller to override the factory used to create
  57. * XMLHttpRequest objects.
  58. * - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
  59. * be removed before parsing the response as JSON.
  60. *
  61. * @typedef {{
  62. * headers: (Object<string>|undefined),
  63. * mimeType: (string|undefined),
  64. * responseType: (xhr.ResponseType|undefined),
  65. * timeoutMs: (number|undefined),
  66. * withCredentials: (boolean|undefined),
  67. * xmlHttpFactory: (goog.net.XmlHttpFactory|undefined),
  68. * xssiPrefix: (string|undefined)
  69. * }}
  70. */
  71. xhr.Options;
  72. /**
  73. * Defines the types that are allowed as post data.
  74. * @typedef {(ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined)}
  75. */
  76. xhr.PostData;
  77. /**
  78. * The Content-Type HTTP header name.
  79. * @type {string}
  80. */
  81. xhr.CONTENT_TYPE_HEADER = 'Content-Type';
  82. /**
  83. * The Content-Type HTTP header value for a url-encoded form.
  84. * @type {string}
  85. */
  86. xhr.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
  87. /**
  88. * Supported data types for the responseType field.
  89. * See: http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-response
  90. * @enum {string}
  91. */
  92. xhr.ResponseType = {
  93. ARRAYBUFFER: 'arraybuffer',
  94. BLOB: 'blob',
  95. DOCUMENT: 'document',
  96. JSON: 'json',
  97. TEXT: 'text'
  98. };
  99. /**
  100. * Sends a get request, returning a promise that will be resolved
  101. * with the response text once the request completes.
  102. *
  103. * @param {string} url The URL to request.
  104. * @param {xhr.Options=} opt_options Configuration options for the request.
  105. * @return {!goog.Promise<string>} A promise that will be resolved with the
  106. * response text once the request completes.
  107. */
  108. xhr.get = function(url, opt_options) {
  109. return xhr.send('GET', url, null, opt_options).then(function(request) {
  110. return request.responseText;
  111. });
  112. };
  113. /**
  114. * Sends a post request, returning a promise that will be resolved
  115. * with the response text once the request completes.
  116. *
  117. * @param {string} url The URL to request.
  118. * @param {xhr.PostData} data The body of the post request.
  119. * @param {xhr.Options=} opt_options Configuration options for the request.
  120. * @return {!goog.Promise<string>} A promise that will be resolved with the
  121. * response text once the request completes.
  122. */
  123. xhr.post = function(url, data, opt_options) {
  124. return xhr.send('POST', url, data, opt_options).then(function(request) {
  125. return request.responseText;
  126. });
  127. };
  128. /**
  129. * Sends a get request, returning a promise that will be resolved with
  130. * the parsed response text once the request completes.
  131. *
  132. * @param {string} url The URL to request.
  133. * @param {xhr.Options=} opt_options Configuration options for the request.
  134. * @return {!goog.Promise<Object>} A promise that will be resolved with the
  135. * response JSON once the request completes.
  136. */
  137. xhr.getJson = function(url, opt_options) {
  138. return xhr.send('GET', url, null, opt_options).then(function(request) {
  139. return xhr.parseJson_(request.responseText, opt_options);
  140. });
  141. };
  142. /**
  143. * Sends a get request, returning a promise that will be resolved with the
  144. * response as a Blob.
  145. *
  146. * @param {string} url The URL to request.
  147. * @param {xhr.Options=} opt_options Configuration options for the request. If
  148. * responseType is set, it will be ignored for this request.
  149. * @return {!goog.Promise<!Blob>} A promise that will be resolved with an
  150. * immutable Blob representing the file once the request completes.
  151. */
  152. xhr.getBlob = function(url, opt_options) {
  153. goog.asserts.assert(
  154. 'Blob' in goog.global, 'getBlob is not supported in this browser.');
  155. var options = opt_options ? goog.object.clone(opt_options) : {};
  156. options.responseType = xhr.ResponseType.BLOB;
  157. return xhr.send('GET', url, null, options).then(function(request) {
  158. return /** @type {!Blob} */ (request.response);
  159. });
  160. };
  161. /**
  162. * Sends a get request, returning a promise that will be resolved with the
  163. * response as an array of bytes.
  164. *
  165. * Supported in all XMLHttpRequest level 2 browsers, as well as IE9. IE8 and
  166. * earlier are not supported.
  167. *
  168. * @param {string} url The URL to request.
  169. * @param {xhr.Options=} opt_options Configuration options for the request. If
  170. * responseType is set, it will be ignored for this request.
  171. * @return {!goog.Promise<!Uint8Array|!Array<number>>} A promise that will be
  172. * resolved with an array of bytes once the request completes.
  173. */
  174. xhr.getBytes = function(url, opt_options) {
  175. goog.asserts.assert(
  176. !userAgent.IE || userAgent.isDocumentModeOrHigher(9),
  177. 'getBytes is not supported in this browser.');
  178. var options = opt_options ? goog.object.clone(opt_options) : {};
  179. options.responseType = xhr.ResponseType.ARRAYBUFFER;
  180. return xhr.send('GET', url, null, options).then(function(request) {
  181. // Use the ArrayBuffer response in browsers that support XMLHttpRequest2.
  182. // This covers nearly all modern browsers: http://caniuse.com/xhr2
  183. if (request.response) {
  184. return new Uint8Array(/** @type {!ArrayBuffer} */ (request.response));
  185. }
  186. // Fallback for IE9: the response may be accessed as an array of bytes with
  187. // the non-standard responseBody property, which can only be accessed as a
  188. // VBArray. IE7 and IE8 require significant amounts of VBScript to extract
  189. // the bytes.
  190. // See: http://stackoverflow.com/questions/1919972/
  191. if (goog.global['VBArray']) {
  192. return new goog.global['VBArray'](request['responseBody']).toArray();
  193. }
  194. // Nearly all common browsers are covered by the cases above. If downloading
  195. // binary files in older browsers is necessary, the MDN article "Sending and
  196. // Receiving Binary Data" provides techniques that may work with
  197. // XMLHttpRequest level 1 browsers: http://goo.gl/7lEuGN
  198. throw new xhr.Error(
  199. 'getBytes is not supported in this browser.', url, request);
  200. });
  201. };
  202. /**
  203. * Sends a post request, returning a promise that will be resolved with
  204. * the parsed response text once the request completes.
  205. *
  206. * @param {string} url The URL to request.
  207. * @param {xhr.PostData} data The body of the post request.
  208. * @param {xhr.Options=} opt_options Configuration options for the request.
  209. * @return {!goog.Promise<Object>} A promise that will be resolved with the
  210. * response JSON once the request completes.
  211. */
  212. xhr.postJson = function(url, data, opt_options) {
  213. return xhr.send('POST', url, data, opt_options).then(function(request) {
  214. return xhr.parseJson_(request.responseText, opt_options);
  215. });
  216. };
  217. /**
  218. * Sends a request, returning a promise that will be resolved
  219. * with the XHR object once the request completes.
  220. *
  221. * If content type hasn't been set in opt_options headers, and hasn't been
  222. * explicitly set to null, default to form-urlencoded/UTF8 for POSTs.
  223. *
  224. * @param {string} method The HTTP method for the request.
  225. * @param {string} url The URL to request.
  226. * @param {xhr.PostData} data The body of the post request.
  227. * @param {xhr.Options=} opt_options Configuration options for the request.
  228. * @return {!goog.Promise<!goog.net.XhrLike.OrNative>} A promise that will be
  229. * resolved with the XHR object once the request completes.
  230. */
  231. xhr.send = function(method, url, data, opt_options) {
  232. return new goog.Promise(function(resolve, reject) {
  233. var options = opt_options || {};
  234. var timer;
  235. var request = options.xmlHttpFactory ?
  236. options.xmlHttpFactory.createInstance() :
  237. goog.net.XmlHttp();
  238. try {
  239. request.open(method, url, true);
  240. } catch (e) {
  241. // XMLHttpRequest.open may throw when 'open' is called, for example, IE7
  242. // throws "Access Denied" for cross-origin requests.
  243. reject(new xhr.Error('Error opening XHR: ' + e.message, url, request));
  244. }
  245. // So sad that IE doesn't support onload and onerror.
  246. request.onreadystatechange = function() {
  247. if (request.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  248. goog.global.clearTimeout(timer);
  249. // Note: When developing locally, XHRs to file:// schemes return
  250. // a status code of 0. We mark that case as a success too.
  251. if (HttpStatus.isSuccess(request.status) ||
  252. request.status === 0 && !xhr.isEffectiveSchemeHttp_(url)) {
  253. resolve(request);
  254. } else {
  255. reject(new xhr.HttpError(request.status, url, request));
  256. }
  257. }
  258. };
  259. request.onerror = function() {
  260. reject(new xhr.Error('Network error', url, request));
  261. };
  262. // Set the headers.
  263. var contentType;
  264. if (options.headers) {
  265. for (var key in options.headers) {
  266. var value = options.headers[key];
  267. if (goog.isDefAndNotNull(value)) {
  268. request.setRequestHeader(key, value);
  269. }
  270. }
  271. contentType = options.headers[xhr.CONTENT_TYPE_HEADER];
  272. }
  273. // Browsers will automatically set the content type to multipart/form-data
  274. // when passed a FormData object.
  275. var dataIsFormData =
  276. (goog.global['FormData'] && (data instanceof goog.global['FormData']));
  277. // If a content type hasn't been set, it hasn't been explicitly set to null,
  278. // and the data isn't a FormData, default to form-urlencoded/UTF8 for POSTs.
  279. // This is because some proxies have been known to reject posts without a
  280. // content-type.
  281. if (method == 'POST' && contentType === undefined && !dataIsFormData) {
  282. request.setRequestHeader(xhr.CONTENT_TYPE_HEADER, xhr.FORM_CONTENT_TYPE);
  283. }
  284. // Set whether to include cookies with cross-domain requests. See:
  285. // http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
  286. if (options.withCredentials) {
  287. request.withCredentials = options.withCredentials;
  288. }
  289. // Allows setting an alternative response type, such as an ArrayBuffer. See:
  290. // http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype
  291. if (options.responseType) {
  292. request.responseType = options.responseType;
  293. }
  294. // Allow the request to override the MIME type of the response. See:
  295. // http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
  296. if (options.mimeType) {
  297. request.overrideMimeType(options.mimeType);
  298. }
  299. // Handle timeouts, if requested.
  300. if (options.timeoutMs > 0) {
  301. timer = goog.global.setTimeout(function() {
  302. // Clear event listener before aborting so the errback will not be
  303. // called twice.
  304. request.onreadystatechange = goog.nullFunction;
  305. request.abort();
  306. reject(new xhr.TimeoutError(url, request));
  307. }, options.timeoutMs);
  308. }
  309. // Trigger the send.
  310. try {
  311. request.send(data);
  312. } catch (e) {
  313. // XMLHttpRequest.send is known to throw on some versions of FF,
  314. // for example if a cross-origin request is disallowed.
  315. request.onreadystatechange = goog.nullFunction;
  316. goog.global.clearTimeout(timer);
  317. reject(new xhr.Error('Error sending XHR: ' + e.message, url, request));
  318. }
  319. });
  320. };
  321. /**
  322. * @param {string} url The URL to test.
  323. * @return {boolean} Whether the effective scheme is HTTP or HTTPs.
  324. * @private
  325. */
  326. xhr.isEffectiveSchemeHttp_ = function(url) {
  327. var scheme = goog.uri.utils.getEffectiveScheme(url);
  328. // NOTE(user): Empty-string is for the case under FF3.5 when the location
  329. // is not defined inside a web worker.
  330. return scheme == 'http' || scheme == 'https' || scheme == '';
  331. };
  332. /**
  333. * JSON-parses the given response text, returning an Object.
  334. *
  335. * @param {string} responseText Response text.
  336. * @param {xhr.Options|undefined} options The options object.
  337. * @return {Object} The JSON-parsed value of the original responseText.
  338. * @private
  339. */
  340. xhr.parseJson_ = function(responseText, options) {
  341. var prefixStrippedResult = responseText;
  342. if (options && options.xssiPrefix) {
  343. prefixStrippedResult =
  344. xhr.stripXssiPrefix_(options.xssiPrefix, prefixStrippedResult);
  345. }
  346. return goog.json.parse(prefixStrippedResult);
  347. };
  348. /**
  349. * Strips the XSSI prefix from the input string.
  350. *
  351. * @param {string} prefix The XSSI prefix.
  352. * @param {string} string The string to strip the prefix from.
  353. * @return {string} The input string without the prefix.
  354. * @private
  355. */
  356. xhr.stripXssiPrefix_ = function(prefix, string) {
  357. if (goog.string.startsWith(string, prefix)) {
  358. string = string.substring(prefix.length);
  359. }
  360. return string;
  361. };
  362. /**
  363. * Generic error that may occur during a request.
  364. *
  365. * @param {string} message The error message.
  366. * @param {string} url The URL that was being requested.
  367. * @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
  368. * @extends {goog.debug.Error}
  369. * @constructor
  370. */
  371. xhr.Error = function(message, url, request) {
  372. xhr.Error.base(this, 'constructor', message + ', url=' + url);
  373. /**
  374. * The URL that was requested.
  375. * @type {string}
  376. */
  377. this.url = url;
  378. /**
  379. * The XMLHttpRequest corresponding with the failed request.
  380. * @type {!goog.net.XhrLike.OrNative}
  381. */
  382. this.xhr = request;
  383. };
  384. goog.inherits(xhr.Error, goog.debug.Error);
  385. /** @override */
  386. xhr.Error.prototype.name = 'XhrError';
  387. /**
  388. * Class for HTTP errors.
  389. *
  390. * @param {number} status The HTTP status code of the response.
  391. * @param {string} url The URL that was being requested.
  392. * @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
  393. * @extends {xhr.Error}
  394. * @constructor
  395. * @final
  396. */
  397. xhr.HttpError = function(status, url, request) {
  398. xhr.HttpError.base(
  399. this, 'constructor', 'Request Failed, status=' + status, url, request);
  400. /**
  401. * The HTTP status code for the error.
  402. * @type {number}
  403. */
  404. this.status = status;
  405. };
  406. goog.inherits(xhr.HttpError, xhr.Error);
  407. /** @override */
  408. xhr.HttpError.prototype.name = 'XhrHttpError';
  409. /**
  410. * Class for Timeout errors.
  411. *
  412. * @param {string} url The URL that timed out.
  413. * @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
  414. * @extends {xhr.Error}
  415. * @constructor
  416. * @final
  417. */
  418. xhr.TimeoutError = function(url, request) {
  419. xhr.TimeoutError.base(this, 'constructor', 'Request timed out', url, request);
  420. };
  421. goog.inherits(xhr.TimeoutError, xhr.Error);
  422. /** @override */
  423. xhr.TimeoutError.prototype.name = 'XhrTimeoutError';
  424. }); // goog.scope