channelrequest.js 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. // Copyright 2006 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 Definition of the ChannelRequest class. The request
  16. * object encapsulates the logic for making a single request, either for the
  17. * forward channel, back channel, or test channel, to the server. It contains
  18. * the logic for the two types of transports we use:
  19. * XMLHTTP and Image request. It provides timeout detection. More transports
  20. * to be added in future, such as Fetch, WebSocket.
  21. *
  22. * @visibility {:internal}
  23. */
  24. goog.provide('goog.labs.net.webChannel.ChannelRequest');
  25. goog.require('goog.Timer');
  26. goog.require('goog.async.Throttle');
  27. goog.require('goog.events.EventHandler');
  28. goog.require('goog.labs.net.webChannel.Channel');
  29. goog.require('goog.labs.net.webChannel.WebChannelDebug');
  30. goog.require('goog.labs.net.webChannel.requestStats');
  31. goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');
  32. goog.require('goog.labs.net.webChannel.requestStats.Stat');
  33. goog.require('goog.net.ErrorCode');
  34. goog.require('goog.net.EventType');
  35. goog.require('goog.net.XmlHttp');
  36. goog.require('goog.object');
  37. goog.require('goog.userAgent');
  38. /**
  39. * A new ChannelRequest is created for each request to the server.
  40. *
  41. * @param {goog.labs.net.webChannel.Channel} channel
  42. * The channel that owns this request.
  43. * @param {goog.labs.net.webChannel.WebChannelDebug} channelDebug A
  44. * WebChannelDebug to use for logging.
  45. * @param {string=} opt_sessionId The session id for the channel.
  46. * @param {string|number=} opt_requestId The request id for this request.
  47. * @param {number=} opt_retryId The retry id for this request.
  48. * @constructor
  49. * @struct
  50. * @final
  51. */
  52. goog.labs.net.webChannel.ChannelRequest = function(
  53. channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
  54. /**
  55. * The channel object that owns the request.
  56. * @private {goog.labs.net.webChannel.Channel}
  57. */
  58. this.channel_ = channel;
  59. /**
  60. * The channel debug to use for logging
  61. * @private {goog.labs.net.webChannel.WebChannelDebug}
  62. */
  63. this.channelDebug_ = channelDebug;
  64. /**
  65. * The Session ID for the channel.
  66. * @private {string|undefined}
  67. */
  68. this.sid_ = opt_sessionId;
  69. /**
  70. * The RID (request ID) for the request.
  71. * @private {string|number|undefined}
  72. */
  73. this.rid_ = opt_requestId;
  74. /**
  75. * The attempt number of the current request.
  76. * @private {number}
  77. */
  78. this.retryId_ = opt_retryId || 1;
  79. /**
  80. * An object to keep track of the channel request event listeners.
  81. * @private {!goog.events.EventHandler<
  82. * !goog.labs.net.webChannel.ChannelRequest>}
  83. */
  84. this.eventHandler_ = new goog.events.EventHandler(this);
  85. /**
  86. * The timeout in ms before failing the request.
  87. * @private {number}
  88. */
  89. this.timeout_ = goog.labs.net.webChannel.ChannelRequest.TIMEOUT_MS_;
  90. /**
  91. * A timer for polling responseText in browsers that don't fire
  92. * onreadystatechange during incremental loading of responseText.
  93. * @private {goog.Timer}
  94. */
  95. this.pollingTimer_ = new goog.Timer();
  96. this.pollingTimer_.setInterval(
  97. goog.labs.net.webChannel.ChannelRequest.POLLING_INTERVAL_MS_);
  98. /**
  99. * Extra HTTP headers to add to all the requests sent to the server.
  100. * @private {Object}
  101. */
  102. this.extraHeaders_ = null;
  103. /**
  104. * Whether the request was successful. This is only set to true after the
  105. * request successfully completes.
  106. * @private {boolean}
  107. */
  108. this.successful_ = false;
  109. /**
  110. * The TimerID of the timer used to detect if the request has timed-out.
  111. * @type {?number}
  112. * @private
  113. */
  114. this.watchDogTimerId_ = null;
  115. /**
  116. * The time in the future when the request will timeout.
  117. * @private {?number}
  118. */
  119. this.watchDogTimeoutTime_ = null;
  120. /**
  121. * The time the request started.
  122. * @private {?number}
  123. */
  124. this.requestStartTime_ = null;
  125. /**
  126. * The type of request (XMLHTTP, IMG)
  127. * @private {?number}
  128. */
  129. this.type_ = null;
  130. /**
  131. * The base Uri for the request. The includes all the parameters except the
  132. * one that indicates the retry number.
  133. * @private {goog.Uri}
  134. */
  135. this.baseUri_ = null;
  136. /**
  137. * The request Uri that was actually used for the most recent request attempt.
  138. * @private {goog.Uri}
  139. */
  140. this.requestUri_ = null;
  141. /**
  142. * The post data, if the request is a post.
  143. * @private {?string}
  144. */
  145. this.postData_ = null;
  146. /**
  147. * The XhrLte request if the request is using XMLHTTP
  148. * @private {goog.net.XhrIo}
  149. */
  150. this.xmlHttp_ = null;
  151. /**
  152. * The position of where the next unprocessed chunk starts in the response
  153. * text.
  154. * @private {number}
  155. */
  156. this.xmlHttpChunkStart_ = 0;
  157. /**
  158. * The verb (Get or Post) for the request.
  159. * @private {?string}
  160. */
  161. this.verb_ = null;
  162. /**
  163. * The last error if the request failed.
  164. * @private {?goog.labs.net.webChannel.ChannelRequest.Error}
  165. */
  166. this.lastError_ = null;
  167. /**
  168. * The last status code received.
  169. * @private {number}
  170. */
  171. this.lastStatusCode_ = -1;
  172. /**
  173. * Whether to send the Connection:close header as part of the request.
  174. * @private {boolean}
  175. */
  176. this.sendClose_ = true;
  177. /**
  178. * Whether the request has been cancelled due to a call to cancel.
  179. * @private {boolean}
  180. */
  181. this.cancelled_ = false;
  182. /**
  183. * A throttle time in ms for readystatechange events for the backchannel.
  184. * Useful for throttling when ready state is INTERACTIVE (partial data).
  185. * If set to zero no throttle is used.
  186. *
  187. * See WebChannelBase.prototype.readyStateChangeThrottleMs_
  188. *
  189. * @private {number}
  190. */
  191. this.readyStateChangeThrottleMs_ = 0;
  192. /**
  193. * The throttle for readystatechange events for the current request, or null
  194. * if there is none.
  195. * @private {goog.async.Throttle}
  196. */
  197. this.readyStateChangeThrottle_ = null;
  198. /**
  199. * Whether to the result is expected to be encoded for chunking and thus
  200. * requires decoding.
  201. * @private {boolean}
  202. */
  203. this.decodeChunks_ = false;
  204. };
  205. goog.scope(function() {
  206. var Channel = goog.labs.net.webChannel.Channel;
  207. var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
  208. var requestStats = goog.labs.net.webChannel.requestStats;
  209. var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
  210. /**
  211. * Default timeout in MS for a request. The server must return data within this
  212. * time limit for the request to not timeout.
  213. * @private {number}
  214. */
  215. ChannelRequest.TIMEOUT_MS_ = 45 * 1000;
  216. /**
  217. * How often to poll (in MS) for changes to responseText in browsers that don't
  218. * fire onreadystatechange during incremental loading of responseText.
  219. * @private {number}
  220. */
  221. ChannelRequest.POLLING_INTERVAL_MS_ = 250;
  222. /**
  223. * Enum for channel requests type
  224. * @enum {number}
  225. * @private
  226. */
  227. ChannelRequest.Type_ = {
  228. /**
  229. * XMLHTTP requests.
  230. */
  231. XML_HTTP: 1,
  232. /**
  233. * IMG requests.
  234. */
  235. CLOSE_REQUEST: 2
  236. };
  237. /**
  238. * Enum type for identifying an error.
  239. * @enum {number}
  240. */
  241. ChannelRequest.Error = {
  242. /**
  243. * Errors due to a non-200 status code.
  244. */
  245. STATUS: 0,
  246. /**
  247. * Errors due to no data being returned.
  248. */
  249. NO_DATA: 1,
  250. /**
  251. * Errors due to a timeout.
  252. */
  253. TIMEOUT: 2,
  254. /**
  255. * Errors due to the server returning an unknown.
  256. */
  257. UNKNOWN_SESSION_ID: 3,
  258. /**
  259. * Errors due to bad data being received.
  260. */
  261. BAD_DATA: 4,
  262. /**
  263. * Errors due to the handler throwing an exception.
  264. */
  265. HANDLER_EXCEPTION: 5,
  266. /**
  267. * The browser declared itself offline during the request.
  268. */
  269. BROWSER_OFFLINE: 6
  270. };
  271. /**
  272. * Returns a useful error string for debugging based on the specified error
  273. * code.
  274. * @param {?ChannelRequest.Error} errorCode The error code.
  275. * @param {number} statusCode The HTTP status code.
  276. * @return {string} The error string for the given code combination.
  277. */
  278. ChannelRequest.errorStringFromCode = function(errorCode, statusCode) {
  279. switch (errorCode) {
  280. case ChannelRequest.Error.STATUS:
  281. return 'Non-200 return code (' + statusCode + ')';
  282. case ChannelRequest.Error.NO_DATA:
  283. return 'XMLHTTP failure (no data)';
  284. case ChannelRequest.Error.TIMEOUT:
  285. return 'HttpConnection timeout';
  286. default:
  287. return 'Unknown error';
  288. }
  289. };
  290. /**
  291. * Sentinel value used to indicate an invalid chunk in a multi-chunk response.
  292. * @private {Object}
  293. */
  294. ChannelRequest.INVALID_CHUNK_ = {};
  295. /**
  296. * Sentinel value used to indicate an incomplete chunk in a multi-chunk
  297. * response.
  298. * @private {Object}
  299. */
  300. ChannelRequest.INCOMPLETE_CHUNK_ = {};
  301. /**
  302. * Returns whether XHR streaming is supported on this browser.
  303. *
  304. * @return {boolean} Whether XHR streaming is supported.
  305. * @see http://code.google.com/p/closure-library/issues/detail?id=346
  306. */
  307. ChannelRequest.supportsXhrStreaming = function() {
  308. return !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(10);
  309. };
  310. /**
  311. * Sets extra HTTP headers to add to all the requests sent to the server.
  312. *
  313. * @param {Object} extraHeaders The HTTP headers.
  314. */
  315. ChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {
  316. this.extraHeaders_ = extraHeaders;
  317. };
  318. /**
  319. * Sets the timeout for a request
  320. *
  321. * @param {number} timeout The timeout in MS for when we fail the request.
  322. */
  323. ChannelRequest.prototype.setTimeout = function(timeout) {
  324. this.timeout_ = timeout;
  325. };
  326. /**
  327. * Sets the throttle for handling onreadystatechange events for the request.
  328. *
  329. * @param {number} throttle The throttle in ms. A value of zero indicates
  330. * no throttle.
  331. */
  332. ChannelRequest.prototype.setReadyStateChangeThrottle = function(throttle) {
  333. this.readyStateChangeThrottleMs_ = throttle;
  334. };
  335. /**
  336. * Uses XMLHTTP to send an HTTP POST to the server.
  337. *
  338. * @param {goog.Uri} uri The uri of the request.
  339. * @param {string} postData The data for the post body.
  340. * @param {boolean} decodeChunks Whether to the result is expected to be
  341. * encoded for chunking and thus requires decoding.
  342. */
  343. ChannelRequest.prototype.xmlHttpPost = function(uri, postData, decodeChunks) {
  344. this.type_ = ChannelRequest.Type_.XML_HTTP;
  345. this.baseUri_ = uri.clone().makeUnique();
  346. this.postData_ = postData;
  347. this.decodeChunks_ = decodeChunks;
  348. this.sendXmlHttp_(null /* hostPrefix */);
  349. };
  350. /**
  351. * Uses XMLHTTP to send an HTTP GET to the server.
  352. *
  353. * @param {goog.Uri} uri The uri of the request.
  354. * @param {boolean} decodeChunks Whether to the result is expected to be
  355. * encoded for chunking and thus requires decoding.
  356. * @param {?string} hostPrefix The host prefix, if we might be using a
  357. * secondary domain. Note that it should also be in the URL, adding this
  358. * won't cause it to be added to the URL.
  359. * @param {boolean=} opt_noClose Whether to request that the tcp/ip connection
  360. * should be closed.
  361. */
  362. ChannelRequest.prototype.xmlHttpGet = function(
  363. uri, decodeChunks, hostPrefix, opt_noClose) {
  364. this.type_ = ChannelRequest.Type_.XML_HTTP;
  365. this.baseUri_ = uri.clone().makeUnique();
  366. this.postData_ = null;
  367. this.decodeChunks_ = decodeChunks;
  368. if (opt_noClose) {
  369. this.sendClose_ = false;
  370. }
  371. this.sendXmlHttp_(hostPrefix);
  372. };
  373. /**
  374. * Sends a request via XMLHTTP according to the current state of the request
  375. * object.
  376. *
  377. * @param {?string} hostPrefix The host prefix, if we might be using a secondary
  378. * domain.
  379. * @private
  380. */
  381. ChannelRequest.prototype.sendXmlHttp_ = function(hostPrefix) {
  382. this.requestStartTime_ = goog.now();
  383. this.ensureWatchDogTimer_();
  384. // clone the base URI to create the request URI. The request uri has the
  385. // attempt number as a parameter which helps in debugging.
  386. this.requestUri_ = this.baseUri_.clone();
  387. this.requestUri_.setParameterValues('t', this.retryId_);
  388. // send the request either as a POST or GET
  389. this.xmlHttpChunkStart_ = 0;
  390. var useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();
  391. this.xmlHttp_ =
  392. this.channel_.createXhrIo(useSecondaryDomains ? hostPrefix : null);
  393. if (this.readyStateChangeThrottleMs_ > 0) {
  394. this.readyStateChangeThrottle_ = new goog.async.Throttle(
  395. goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),
  396. this.readyStateChangeThrottleMs_);
  397. }
  398. this.eventHandler_.listen(
  399. this.xmlHttp_, goog.net.EventType.READY_STATE_CHANGE,
  400. this.readyStateChangeHandler_);
  401. var headers = this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};
  402. if (this.postData_) {
  403. this.verb_ = 'POST';
  404. headers['Content-Type'] = 'application/x-www-form-urlencoded';
  405. this.xmlHttp_.send(this.requestUri_, this.verb_, this.postData_, headers);
  406. } else {
  407. this.verb_ = 'GET';
  408. // If the user agent is webkit, we cannot send the close header since it is
  409. // disallowed by the browser. If we attempt to set the "Connection: close"
  410. // header in WEBKIT browser, it will actually causes an error message.
  411. if (this.sendClose_ && !goog.userAgent.WEBKIT) {
  412. headers['Connection'] = 'close';
  413. }
  414. this.xmlHttp_.send(this.requestUri_, this.verb_, null, headers);
  415. }
  416. requestStats.notifyServerReachabilityEvent(
  417. requestStats.ServerReachability.REQUEST_MADE);
  418. this.channelDebug_.xmlHttpChannelRequest(
  419. this.verb_, this.requestUri_, this.rid_, this.retryId_, this.postData_);
  420. };
  421. /**
  422. * Handles a readystatechange event.
  423. * @param {goog.events.Event} evt The event.
  424. * @private
  425. */
  426. ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {
  427. var xhr = /** @type {goog.net.XhrIo} */ (evt.target);
  428. var throttle = this.readyStateChangeThrottle_;
  429. if (throttle &&
  430. xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
  431. // Only throttle in the partial data case.
  432. this.channelDebug_.debug('Throttling readystatechange.');
  433. throttle.fire();
  434. } else {
  435. // If we haven't throttled, just handle response directly.
  436. this.xmlHttpHandler_(xhr);
  437. }
  438. };
  439. /**
  440. * XmlHttp handler
  441. * @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.
  442. * @private
  443. */
  444. ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {
  445. requestStats.onStartExecution();
  446. try {
  447. if (xmlhttp == this.xmlHttp_) {
  448. this.onXmlHttpReadyStateChanged_();
  449. } else {
  450. this.channelDebug_.warning(
  451. 'Called back with an ' +
  452. 'unexpected xmlhttp');
  453. }
  454. } catch (ex) {
  455. this.channelDebug_.debug('Failed call to OnXmlHttpReadyStateChanged_');
  456. if (this.xmlHttp_ && this.xmlHttp_.getResponseText()) {
  457. this.channelDebug_.dumpException(
  458. ex, 'ResponseText: ' + this.xmlHttp_.getResponseText());
  459. } else {
  460. this.channelDebug_.dumpException(ex, 'No response text');
  461. }
  462. } finally {
  463. requestStats.onEndExecution();
  464. }
  465. };
  466. /**
  467. * Called by the readystate handler for XMLHTTP requests.
  468. *
  469. * @private
  470. */
  471. ChannelRequest.prototype.onXmlHttpReadyStateChanged_ = function() {
  472. var readyState = this.xmlHttp_.getReadyState();
  473. var errorCode = this.xmlHttp_.getLastErrorCode();
  474. var statusCode = this.xmlHttp_.getStatus();
  475. // we get partial results in browsers that support ready state interactive.
  476. // We also make sure that getResponseText is not null in interactive mode
  477. // before we continue. However, we don't do it in Opera because it only
  478. // fire readyState == INTERACTIVE once. We need the following code to poll
  479. if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
  480. readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&
  481. !goog.userAgent.OPERA && !this.xmlHttp_.getResponseText()) {
  482. // not yet ready
  483. return;
  484. }
  485. // Dispatch any appropriate network events.
  486. if (!this.cancelled_ && readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
  487. errorCode != goog.net.ErrorCode.ABORT) {
  488. // Pretty conservative, these are the only known scenarios which we'd
  489. // consider indicative of a truly non-functional network connection.
  490. if (errorCode == goog.net.ErrorCode.TIMEOUT || statusCode <= 0) {
  491. requestStats.notifyServerReachabilityEvent(
  492. requestStats.ServerReachability.REQUEST_FAILED);
  493. } else {
  494. requestStats.notifyServerReachabilityEvent(
  495. requestStats.ServerReachability.REQUEST_SUCCEEDED);
  496. }
  497. }
  498. // got some data so cancel the watchdog timer
  499. this.cancelWatchDogTimer_();
  500. var status = this.xmlHttp_.getStatus();
  501. this.lastStatusCode_ = status;
  502. var responseText = this.xmlHttp_.getResponseText();
  503. if (!responseText) {
  504. this.channelDebug_.debug(
  505. 'No response text for uri ' + this.requestUri_ + ' status ' + status);
  506. }
  507. this.successful_ = (status == 200);
  508. this.channelDebug_.xmlHttpChannelResponseMetaData(
  509. /** @type {string} */ (this.verb_), this.requestUri_, this.rid_,
  510. this.retryId_, readyState, status);
  511. if (!this.successful_) {
  512. if (status == 400 && responseText.indexOf('Unknown SID') > 0) {
  513. // the server error string will include 'Unknown SID' which indicates the
  514. // server doesn't know about the session (maybe it got restarted, maybe
  515. // the user got moved to another server, etc.,). Handlers can special
  516. // case this error
  517. this.lastError_ = ChannelRequest.Error.UNKNOWN_SESSION_ID;
  518. requestStats.notifyStatEvent(
  519. requestStats.Stat.REQUEST_UNKNOWN_SESSION_ID);
  520. this.channelDebug_.warning('XMLHTTP Unknown SID (' + this.rid_ + ')');
  521. } else {
  522. this.lastError_ = ChannelRequest.Error.STATUS;
  523. requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_STATUS);
  524. this.channelDebug_.warning(
  525. 'XMLHTTP Bad status ' + status + ' (' + this.rid_ + ')');
  526. }
  527. this.cleanup_();
  528. this.dispatchFailure_();
  529. return;
  530. }
  531. if (this.decodeChunks_) {
  532. this.decodeNextChunks_(readyState, responseText);
  533. if (goog.userAgent.OPERA && this.successful_ &&
  534. readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
  535. this.startPolling_();
  536. }
  537. } else {
  538. this.channelDebug_.xmlHttpChannelResponseText(
  539. this.rid_, responseText, null);
  540. this.safeOnRequestData_(responseText);
  541. }
  542. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  543. this.cleanup_();
  544. }
  545. if (!this.successful_) {
  546. return;
  547. }
  548. if (!this.cancelled_) {
  549. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  550. this.channel_.onRequestComplete(this);
  551. } else {
  552. // The default is false, the result from this callback shouldn't carry
  553. // over to the next callback, otherwise the request looks successful if
  554. // the watchdog timer gets called
  555. this.successful_ = false;
  556. this.ensureWatchDogTimer_();
  557. }
  558. }
  559. };
  560. /**
  561. * Decodes the next set of available chunks in the response.
  562. * @param {number} readyState The value of readyState.
  563. * @param {string} responseText The value of responseText.
  564. * @private
  565. */
  566. ChannelRequest.prototype.decodeNextChunks_ = function(
  567. readyState, responseText) {
  568. var decodeNextChunksSuccessful = true;
  569. while (!this.cancelled_ && this.xmlHttpChunkStart_ < responseText.length) {
  570. var chunkText = this.getNextChunk_(responseText);
  571. if (chunkText == ChannelRequest.INCOMPLETE_CHUNK_) {
  572. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
  573. // should have consumed entire response when the request is done
  574. this.lastError_ = ChannelRequest.Error.BAD_DATA;
  575. requestStats.notifyStatEvent(requestStats.Stat.REQUEST_INCOMPLETE_DATA);
  576. decodeNextChunksSuccessful = false;
  577. }
  578. this.channelDebug_.xmlHttpChannelResponseText(
  579. this.rid_, null, '[Incomplete Response]');
  580. break;
  581. } else if (chunkText == ChannelRequest.INVALID_CHUNK_) {
  582. this.lastError_ = ChannelRequest.Error.BAD_DATA;
  583. requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_DATA);
  584. this.channelDebug_.xmlHttpChannelResponseText(
  585. this.rid_, responseText, '[Invalid Chunk]');
  586. decodeNextChunksSuccessful = false;
  587. break;
  588. } else {
  589. this.channelDebug_.xmlHttpChannelResponseText(
  590. this.rid_, /** @type {string} */ (chunkText), null);
  591. this.safeOnRequestData_(/** @type {string} */ (chunkText));
  592. }
  593. }
  594. if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
  595. responseText.length == 0) {
  596. // also an error if we didn't get any response
  597. this.lastError_ = ChannelRequest.Error.NO_DATA;
  598. requestStats.notifyStatEvent(requestStats.Stat.REQUEST_NO_DATA);
  599. decodeNextChunksSuccessful = false;
  600. }
  601. this.successful_ = this.successful_ && decodeNextChunksSuccessful;
  602. if (!decodeNextChunksSuccessful) {
  603. // malformed response - we make this trigger retry logic
  604. this.channelDebug_.xmlHttpChannelResponseText(
  605. this.rid_, responseText, '[Invalid Chunked Response]');
  606. this.cleanup_();
  607. this.dispatchFailure_();
  608. }
  609. };
  610. /**
  611. * Polls the response for new data.
  612. * @private
  613. */
  614. ChannelRequest.prototype.pollResponse_ = function() {
  615. var readyState = this.xmlHttp_.getReadyState();
  616. var responseText = this.xmlHttp_.getResponseText();
  617. if (this.xmlHttpChunkStart_ < responseText.length) {
  618. this.cancelWatchDogTimer_();
  619. this.decodeNextChunks_(readyState, responseText);
  620. if (this.successful_ &&
  621. readyState != goog.net.XmlHttp.ReadyState.COMPLETE) {
  622. this.ensureWatchDogTimer_();
  623. }
  624. }
  625. };
  626. /**
  627. * Starts a polling interval for changes to responseText of the
  628. * XMLHttpRequest, for browsers that don't fire onreadystatechange
  629. * as data comes in incrementally. This timer is disabled in
  630. * cleanup_().
  631. * @private
  632. */
  633. ChannelRequest.prototype.startPolling_ = function() {
  634. this.eventHandler_.listen(
  635. this.pollingTimer_, goog.Timer.TICK, this.pollResponse_);
  636. this.pollingTimer_.start();
  637. };
  638. /**
  639. * Returns the next chunk of a chunk-encoded response. This is not standard
  640. * HTTP chunked encoding because browsers don't expose the chunk boundaries to
  641. * the application through XMLHTTP. So we have an additional chunk encoding at
  642. * the application level that lets us tell where the beginning and end of
  643. * individual responses are so that we can only try to eval a complete JS array.
  644. *
  645. * The encoding is the size of the chunk encoded as a decimal string followed
  646. * by a newline followed by the data.
  647. *
  648. * @param {string} responseText The response text from the XMLHTTP response.
  649. * @return {string|Object} The next chunk string or a sentinel object
  650. * indicating a special condition.
  651. * @private
  652. */
  653. ChannelRequest.prototype.getNextChunk_ = function(responseText) {
  654. var sizeStartIndex = this.xmlHttpChunkStart_;
  655. var sizeEndIndex = responseText.indexOf('\n', sizeStartIndex);
  656. if (sizeEndIndex == -1) {
  657. return ChannelRequest.INCOMPLETE_CHUNK_;
  658. }
  659. var sizeAsString = responseText.substring(sizeStartIndex, sizeEndIndex);
  660. var size = Number(sizeAsString);
  661. if (isNaN(size)) {
  662. return ChannelRequest.INVALID_CHUNK_;
  663. }
  664. var chunkStartIndex = sizeEndIndex + 1;
  665. if (chunkStartIndex + size > responseText.length) {
  666. return ChannelRequest.INCOMPLETE_CHUNK_;
  667. }
  668. var chunkText = responseText.substr(chunkStartIndex, size);
  669. this.xmlHttpChunkStart_ = chunkStartIndex + size;
  670. return chunkText;
  671. };
  672. /**
  673. * Uses an IMG tag or navigator.sendBeacon to send an HTTP get to the server.
  674. *
  675. * This is only currently used to terminate the connection, as an IMG tag is
  676. * the most reliable way to send something to the server while the page
  677. * is getting torn down.
  678. *
  679. * Navigator.sendBeacon is available on Chrome and Firefox as a formal
  680. * solution to ensure delivery without blocking window close. See
  681. * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
  682. *
  683. * For Chrome Apps, sendBeacon is always necessary due to Content Security
  684. * Policy (CSP) violation of using an IMG tag.
  685. *
  686. * @param {goog.Uri} uri The uri to send a request to.
  687. */
  688. ChannelRequest.prototype.sendCloseRequest = function(uri) {
  689. this.type_ = ChannelRequest.Type_.CLOSE_REQUEST;
  690. this.baseUri_ = uri.clone().makeUnique();
  691. var requestSent = false;
  692. if (goog.global.navigator && goog.global.navigator.sendBeacon) {
  693. // empty string body to avoid 413 error on chrome < 41
  694. requestSent =
  695. goog.global.navigator.sendBeacon(this.baseUri_.toString(), '');
  696. }
  697. if (!requestSent) {
  698. var eltImg = new Image();
  699. eltImg.src = this.baseUri_;
  700. }
  701. this.requestStartTime_ = goog.now();
  702. this.ensureWatchDogTimer_();
  703. };
  704. /**
  705. * Cancels the request no matter what the underlying transport is.
  706. */
  707. ChannelRequest.prototype.cancel = function() {
  708. this.cancelled_ = true;
  709. this.cleanup_();
  710. };
  711. /**
  712. * Ensures that there is watchdog timeout which is used to ensure that
  713. * the connection completes in time.
  714. *
  715. * @private
  716. */
  717. ChannelRequest.prototype.ensureWatchDogTimer_ = function() {
  718. this.watchDogTimeoutTime_ = goog.now() + this.timeout_;
  719. this.startWatchDogTimer_(this.timeout_);
  720. };
  721. /**
  722. * Starts the watchdog timer which is used to ensure that the connection
  723. * completes in time.
  724. * @param {number} time The number of milliseconds to wait.
  725. * @private
  726. */
  727. ChannelRequest.prototype.startWatchDogTimer_ = function(time) {
  728. if (this.watchDogTimerId_ != null) {
  729. // assertion
  730. throw Error('WatchDog timer not null');
  731. }
  732. this.watchDogTimerId_ =
  733. requestStats.setTimeout(goog.bind(this.onWatchDogTimeout_, this), time);
  734. };
  735. /**
  736. * Cancels the watchdog timer if it has been started.
  737. *
  738. * @private
  739. */
  740. ChannelRequest.prototype.cancelWatchDogTimer_ = function() {
  741. if (this.watchDogTimerId_) {
  742. goog.global.clearTimeout(this.watchDogTimerId_);
  743. this.watchDogTimerId_ = null;
  744. }
  745. };
  746. /**
  747. * Called when the watchdog timer is triggered. It also handles a case where it
  748. * is called too early which we suspect may be happening sometimes
  749. * (not sure why)
  750. *
  751. * @private
  752. */
  753. ChannelRequest.prototype.onWatchDogTimeout_ = function() {
  754. this.watchDogTimerId_ = null;
  755. var now = goog.now();
  756. if (now - this.watchDogTimeoutTime_ >= 0) {
  757. this.handleTimeout_();
  758. } else {
  759. // got called too early for some reason
  760. this.channelDebug_.warning('WatchDog timer called too early');
  761. this.startWatchDogTimer_(this.watchDogTimeoutTime_ - now);
  762. }
  763. };
  764. /**
  765. * Called when the request has actually timed out. Will cleanup and notify the
  766. * channel of the failure.
  767. *
  768. * @private
  769. */
  770. ChannelRequest.prototype.handleTimeout_ = function() {
  771. if (this.successful_) {
  772. // Should never happen.
  773. this.channelDebug_.severe(
  774. 'Received watchdog timeout even though request loaded successfully');
  775. }
  776. this.channelDebug_.timeoutResponse(this.requestUri_);
  777. // IMG or SendBeacon requests never notice if they were successful,
  778. // and always 'time out'. This fact says nothing about reachability.
  779. if (this.type_ != ChannelRequest.Type_.CLOSE_REQUEST) {
  780. requestStats.notifyServerReachabilityEvent(
  781. requestStats.ServerReachability.REQUEST_FAILED);
  782. requestStats.notifyStatEvent(requestStats.Stat.REQUEST_TIMEOUT);
  783. }
  784. this.cleanup_();
  785. // Set error and dispatch failure.
  786. // This is called for CLOSE_REQUEST too to ensure channel_.onRequestComplete.
  787. this.lastError_ = ChannelRequest.Error.TIMEOUT;
  788. this.dispatchFailure_();
  789. };
  790. /**
  791. * Notifies the channel that this request failed.
  792. * @private
  793. */
  794. ChannelRequest.prototype.dispatchFailure_ = function() {
  795. if (this.channel_.isClosed() || this.cancelled_) {
  796. return;
  797. }
  798. this.channel_.onRequestComplete(this);
  799. };
  800. /**
  801. * Cleans up the objects used to make the request. This function is
  802. * idempotent.
  803. *
  804. * @private
  805. */
  806. ChannelRequest.prototype.cleanup_ = function() {
  807. this.cancelWatchDogTimer_();
  808. goog.dispose(this.readyStateChangeThrottle_);
  809. this.readyStateChangeThrottle_ = null;
  810. // Stop the polling timer, if necessary.
  811. this.pollingTimer_.stop();
  812. // Unhook all event handlers.
  813. this.eventHandler_.removeAll();
  814. if (this.xmlHttp_) {
  815. // clear out this.xmlHttp_ before aborting so we handle getting reentered
  816. // inside abort
  817. var xmlhttp = this.xmlHttp_;
  818. this.xmlHttp_ = null;
  819. xmlhttp.abort();
  820. xmlhttp.dispose();
  821. }
  822. };
  823. /**
  824. * Indicates whether the request was successful. Only valid after the handler
  825. * is called to indicate completion of the request.
  826. *
  827. * @return {boolean} True if the request succeeded.
  828. */
  829. ChannelRequest.prototype.getSuccess = function() {
  830. return this.successful_;
  831. };
  832. /**
  833. * If the request was not successful, returns the reason.
  834. *
  835. * @return {?ChannelRequest.Error} The last error.
  836. */
  837. ChannelRequest.prototype.getLastError = function() {
  838. return this.lastError_;
  839. };
  840. /**
  841. * Returns the status code of the last request.
  842. * @return {number} The status code of the last request.
  843. */
  844. ChannelRequest.prototype.getLastStatusCode = function() {
  845. return this.lastStatusCode_;
  846. };
  847. /**
  848. * Returns the session id for this channel.
  849. *
  850. * @return {string|undefined} The session ID.
  851. */
  852. ChannelRequest.prototype.getSessionId = function() {
  853. return this.sid_;
  854. };
  855. /**
  856. * Returns the request id for this request. Each request has a unique request
  857. * id and the request IDs are a sequential increasing count.
  858. *
  859. * @return {string|number|undefined} The request ID.
  860. */
  861. ChannelRequest.prototype.getRequestId = function() {
  862. return this.rid_;
  863. };
  864. /**
  865. * Returns the data for a post, if this request is a post.
  866. *
  867. * @return {?string} The POST data provided by the request initiator.
  868. */
  869. ChannelRequest.prototype.getPostData = function() {
  870. return this.postData_;
  871. };
  872. /**
  873. * Returns the XhrIo request object.
  874. *
  875. * @return {?goog.net.XhrIo} Any XhrIo request created for this object.
  876. */
  877. ChannelRequest.prototype.getXhr = function() {
  878. return this.xmlHttp_;
  879. };
  880. /**
  881. * Returns the time that the request started, if it has started.
  882. *
  883. * @return {?number} The time the request started, as returned by goog.now().
  884. */
  885. ChannelRequest.prototype.getRequestStartTime = function() {
  886. return this.requestStartTime_;
  887. };
  888. /**
  889. * Helper to call the callback's onRequestData, which catches any
  890. * exception and cleans up the request.
  891. * @param {string} data The request data.
  892. * @private
  893. */
  894. ChannelRequest.prototype.safeOnRequestData_ = function(data) {
  895. try {
  896. this.channel_.onRequestData(this, data);
  897. var stats = requestStats.ServerReachability;
  898. requestStats.notifyServerReachabilityEvent(stats.BACK_CHANNEL_ACTIVITY);
  899. } catch (e) {
  900. // Dump debug info, but keep going without closing the channel.
  901. this.channelDebug_.dumpException(e, 'Error in httprequest callback');
  902. }
  903. };
  904. /**
  905. * Convenience factory method.
  906. *
  907. * @param {Channel} channel The channel object that owns this request.
  908. * @param {WebChannelDebug} channelDebug A WebChannelDebug to use for logging.
  909. * @param {string=} opt_sessionId The session id for the channel.
  910. * @param {string|number=} opt_requestId The request id for this request.
  911. * @param {number=} opt_retryId The retry id for this request.
  912. * @return {!ChannelRequest} The created channel request.
  913. */
  914. ChannelRequest.createChannelRequest = function(
  915. channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
  916. return new ChannelRequest(
  917. channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId);
  918. };
  919. }); // goog.scope