xhrstreamreader.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. // Copyright 2015 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 the XHR stream reader implements a low-level stream
  16. * reader for handling a streamed XHR response body. The reader takes a
  17. * StreamParser which may support JSON or any other formats as confirmed by
  18. * the Content-Type of the response. The reader may be used as polyfill for
  19. * different streams APIs such as Node streams or whatwg streams (Fetch).
  20. *
  21. * The first version of this implementation only covers functions necessary
  22. * to support NodeReadableStream. In a later version, this reader will also
  23. * be adapted to whatwg streams.
  24. *
  25. * For IE, only IE-10 and above are supported.
  26. *
  27. * TODO(user): xhr polling, stream timeout, CORS and preflight optimization.
  28. */
  29. goog.provide('goog.net.streams.XhrStreamReader');
  30. goog.require('goog.events.EventHandler');
  31. goog.require('goog.log');
  32. goog.require('goog.net.ErrorCode');
  33. goog.require('goog.net.EventType');
  34. goog.require('goog.net.HttpStatus');
  35. goog.require('goog.net.XhrIo');
  36. goog.require('goog.net.XmlHttp');
  37. goog.require('goog.net.streams.Base64PbStreamParser');
  38. goog.require('goog.net.streams.JsonStreamParser');
  39. goog.require('goog.net.streams.PbJsonStreamParser');
  40. goog.require('goog.net.streams.PbStreamParser');
  41. goog.require('goog.string');
  42. goog.require('goog.userAgent');
  43. goog.scope(function() {
  44. var Base64PbStreamParser =
  45. goog.module.get('goog.net.streams.Base64PbStreamParser');
  46. var PbJsonStreamParser = goog.module.get('goog.net.streams.PbJsonStreamParser');
  47. /**
  48. * The XhrStreamReader class.
  49. *
  50. * The caller must check isStreamingSupported() first.
  51. *
  52. * @param {!goog.net.XhrIo} xhr The XhrIo object with its response body to
  53. * be handled by NodeReadableStream.
  54. * @constructor
  55. * @struct
  56. * @final
  57. * @package
  58. */
  59. goog.net.streams.XhrStreamReader = function(xhr) {
  60. /**
  61. * @const
  62. * @private {?goog.log.Logger} the logger.
  63. */
  64. this.logger_ = goog.log.getLogger('goog.net.streams.XhrStreamReader');
  65. /**
  66. * The xhr object passed by the application.
  67. *
  68. * @private {?goog.net.XhrIo} the XHR object for the stream.
  69. */
  70. this.xhr_ = xhr;
  71. /**
  72. * To be initialized with the correct content-type.
  73. *
  74. * @private {?goog.net.streams.StreamParser} the parser for the stream.
  75. */
  76. this.parser_ = null;
  77. /**
  78. * The position of where the next unprocessed data starts in the XHR
  79. * response text.
  80. * @private {number}
  81. */
  82. this.pos_ = 0;
  83. /**
  84. * The status (error detail) of the current stream.
  85. * @private {!goog.net.streams.XhrStreamReader.Status}
  86. */
  87. this.status_ = goog.net.streams.XhrStreamReader.Status.INIT;
  88. /**
  89. * The handler for any status change event.
  90. *
  91. * @private {?function()} The call back to handle the XHR status change.
  92. */
  93. this.statusHandler_ = null;
  94. /**
  95. * The handler for new response data.
  96. *
  97. * @private {?function(!Array<!Object>)} The call back to handle new
  98. * response data, parsed as an array of atomic messages.
  99. */
  100. this.dataHandler_ = null;
  101. /**
  102. * An object to keep track of event listeners.
  103. *
  104. * @private {!goog.events.EventHandler<!goog.net.streams.XhrStreamReader>}
  105. */
  106. this.eventHandler_ = new goog.events.EventHandler(this);
  107. // register the XHR event handler
  108. this.eventHandler_.listen(
  109. this.xhr_, goog.net.EventType.READY_STATE_CHANGE,
  110. this.readyStateChangeHandler_);
  111. };
  112. /**
  113. * Enum type for current stream status.
  114. * @enum {number}
  115. */
  116. goog.net.streams.XhrStreamReader.Status = {
  117. /**
  118. * Init status, with xhr inactive.
  119. */
  120. INIT: 0,
  121. /**
  122. * XHR being sent.
  123. */
  124. ACTIVE: 1,
  125. /**
  126. * The request was successful, after the request successfully completes.
  127. */
  128. SUCCESS: 2,
  129. /**
  130. * Errors due to a non-200 status code or other error conditions.
  131. */
  132. XHR_ERROR: 3,
  133. /**
  134. * Errors due to no data being returned.
  135. */
  136. NO_DATA: 4,
  137. /**
  138. * Errors due to corrupted or invalid data being received.
  139. */
  140. BAD_DATA: 5,
  141. /**
  142. * Errors due to the handler throwing an exception.
  143. */
  144. HANDLER_EXCEPTION: 6,
  145. /**
  146. * Errors due to a timeout.
  147. */
  148. TIMEOUT: 7,
  149. /**
  150. * The request is cancelled by the application.
  151. */
  152. CANCELLED: 8
  153. };
  154. /**
  155. * Returns whether response streaming is supported on this browser.
  156. *
  157. * @return {boolean} false if response streaming is not supported.
  158. */
  159. goog.net.streams.XhrStreamReader.isStreamingSupported = function() {
  160. if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {
  161. // No active-x due to security issues.
  162. return false;
  163. }
  164. if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('420+')) {
  165. // Safari 3+
  166. // Older versions of Safari always receive null response in INTERACTIVE.
  167. return false;
  168. }
  169. if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {
  170. // Old Opera fires readyState == INTERACTIVE once.
  171. // TODO(user): polling the buffer and check the exact Opera version
  172. return false;
  173. }
  174. return true;
  175. };
  176. /**
  177. * Returns a parser that supports the given content-type (mime) and
  178. * content-transfer-encoding.
  179. *
  180. * @return {?goog.net.streams.StreamParser} a parser or null if the content
  181. * type or transfer encoding is unsupported.
  182. * @private
  183. */
  184. goog.net.streams.XhrStreamReader.prototype.getParserByResponseHeader_ =
  185. function() {
  186. var contentType =
  187. this.xhr_.getStreamingResponseHeader(goog.net.XhrIo.CONTENT_TYPE_HEADER);
  188. if (!contentType) {
  189. goog.log.warning(this.logger_, 'Content-Type unavailable: ' + contentType);
  190. return null;
  191. }
  192. contentType = contentType.toLowerCase();
  193. if (goog.string.startsWith(contentType, 'application/json')) {
  194. if (goog.string.startsWith(contentType, 'application/json+protobuf')) {
  195. return new PbJsonStreamParser();
  196. }
  197. return new goog.net.streams.JsonStreamParser();
  198. }
  199. if (goog.string.startsWith(contentType, 'application/x-protobuf')) {
  200. var encoding = this.xhr_.getStreamingResponseHeader(
  201. goog.net.XhrIo.CONTENT_TRANSFER_ENCODING);
  202. if (!encoding) {
  203. return new goog.net.streams.PbStreamParser();
  204. }
  205. if (encoding.toLowerCase() == 'base64') {
  206. return new Base64PbStreamParser();
  207. }
  208. goog.log.warning(
  209. this.logger_, 'Unsupported Content-Transfer-Encoding: ' + encoding +
  210. '\nFor Content-Type: ' + contentType);
  211. return null;
  212. }
  213. goog.log.warning(this.logger_, 'Unsupported Content-Type: ' + contentType);
  214. return null;
  215. };
  216. /**
  217. * Returns the XHR request object.
  218. *
  219. * @return {goog.net.XhrIo} The XHR object associated with this reader, or
  220. * null if the reader has been cleared.
  221. */
  222. goog.net.streams.XhrStreamReader.prototype.getXhr = function() {
  223. return this.xhr_;
  224. };
  225. /**
  226. * Gets the current stream status.
  227. *
  228. * @return {!goog.net.streams.XhrStreamReader.Status} The stream status.
  229. */
  230. goog.net.streams.XhrStreamReader.prototype.getStatus = function() {
  231. return this.status_;
  232. };
  233. /**
  234. * Sets the status handler.
  235. *
  236. * @param {function()} handler The handler for any status change.
  237. */
  238. goog.net.streams.XhrStreamReader.prototype.setStatusHandler = function(
  239. handler) {
  240. this.statusHandler_ = handler;
  241. };
  242. /**
  243. * Sets the data handler.
  244. *
  245. * @param {function(!Array<!Object>)} handler The handler for new data.
  246. */
  247. goog.net.streams.XhrStreamReader.prototype.setDataHandler = function(handler) {
  248. this.dataHandler_ = handler;
  249. };
  250. /**
  251. * Handles XHR readystatechange events.
  252. *
  253. * TODO(user): throttling may be needed.
  254. *
  255. * @param {!goog.events.Event} event The event.
  256. * @private
  257. */
  258. goog.net.streams.XhrStreamReader.prototype.readyStateChangeHandler_ = function(
  259. event) {
  260. var xhr = /** @type {goog.net.XhrIo} */ (event.target);
  261. try {
  262. if (xhr == this.xhr_) {
  263. this.onReadyStateChanged_();
  264. } else {
  265. goog.log.warning(this.logger_, 'Called back with an unexpected xhr.');
  266. }
  267. } catch (ex) {
  268. goog.log.error(
  269. this.logger_, 'readyStateChangeHandler_ thrown exception' +
  270. ' ' + ex);
  271. // no rethrow
  272. this.updateStatus_(
  273. goog.net.streams.XhrStreamReader.Status.HANDLER_EXCEPTION);
  274. this.clear_();
  275. }
  276. };
  277. /**
  278. * Called from readyStateChangeHandler_.
  279. *
  280. * @private
  281. */
  282. goog.net.streams.XhrStreamReader.prototype.onReadyStateChanged_ = function() {
  283. var readyState = this.xhr_.getReadyState();
  284. var errorCode = this.xhr_.getLastErrorCode();
  285. var statusCode = this.xhr_.getStatus();
  286. var responseText = this.xhr_.getResponseText();
  287. // we get partial results in browsers that support ready state interactive.
  288. // We also make sure that getResponseText is not null in interactive mode
  289. // before we continue.
  290. if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
  291. readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE && !responseText) {
  292. return;
  293. }
  294. // TODO(user): white-list other 2xx responses with application payload
  295. var successful =
  296. (statusCode == goog.net.HttpStatus.OK ||
  297. statusCode == goog.net.HttpStatus.PARTIAL_CONTENT);
  298. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  299. if (errorCode == goog.net.ErrorCode.TIMEOUT) {
  300. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.TIMEOUT);
  301. } else if (errorCode == goog.net.ErrorCode.ABORT) {
  302. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.CANCELLED);
  303. } else if (!successful) {
  304. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.XHR_ERROR);
  305. }
  306. }
  307. if (successful && !responseText) {
  308. goog.log.warning(
  309. this.logger_, 'No response text for xhr ' + this.xhr_.getLastUri() +
  310. ' status ' + statusCode);
  311. }
  312. if (!this.parser_) {
  313. this.parser_ = this.getParserByResponseHeader_();
  314. if (this.parser_ == null) {
  315. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
  316. }
  317. }
  318. if (this.status_ > goog.net.streams.XhrStreamReader.Status.SUCCESS) {
  319. this.clear_();
  320. return;
  321. }
  322. // Parses and delivers any new data, with error status.
  323. if (responseText.length > this.pos_) {
  324. var newData = responseText.substr(this.pos_);
  325. this.pos_ = responseText.length;
  326. try {
  327. var messages = this.parser_.parse(newData);
  328. if (messages != null) {
  329. if (this.dataHandler_) {
  330. this.dataHandler_(messages);
  331. }
  332. }
  333. } catch (ex) {
  334. goog.log.error(
  335. this.logger_, 'Invalid response ' + ex + '\n' + responseText);
  336. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.BAD_DATA);
  337. this.clear_();
  338. return;
  339. }
  340. }
  341. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  342. if (responseText.length == 0) {
  343. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.NO_DATA);
  344. } else {
  345. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.SUCCESS);
  346. }
  347. this.clear_();
  348. return;
  349. }
  350. this.updateStatus_(goog.net.streams.XhrStreamReader.Status.ACTIVE);
  351. };
  352. /**
  353. * Update the status and may call the handler.
  354. *
  355. * @param {!goog.net.streams.XhrStreamReader.Status} status The new status
  356. * @private
  357. */
  358. goog.net.streams.XhrStreamReader.prototype.updateStatus_ = function(status) {
  359. var current = this.status_;
  360. if (current != status) {
  361. this.status_ = status;
  362. if (this.statusHandler_) {
  363. this.statusHandler_();
  364. }
  365. }
  366. };
  367. /**
  368. * Clears after the XHR terminal state is reached.
  369. *
  370. * @private
  371. */
  372. goog.net.streams.XhrStreamReader.prototype.clear_ = function() {
  373. this.eventHandler_.removeAll();
  374. if (this.xhr_) {
  375. // clear out before aborting to avoid being reentered inside abort
  376. var xhr = this.xhr_;
  377. this.xhr_ = null;
  378. xhr.abort();
  379. xhr.dispose();
  380. }
  381. };
  382. }); // goog.scope