Path: blob/trunk/third_party/closure/goog/labs/net/webchannel/channelrequest.js
1865 views
// Copyright 2006 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Definition of the ChannelRequest class. The request16* object encapsulates the logic for making a single request, either for the17* forward channel, back channel, or test channel, to the server. It contains18* the logic for the two types of transports we use:19* XMLHTTP and Image request. It provides timeout detection. More transports20* to be added in future, such as Fetch, WebSocket.21*22* @visibility {:internal}23*/242526goog.provide('goog.labs.net.webChannel.ChannelRequest');2728goog.require('goog.Timer');29goog.require('goog.async.Throttle');30goog.require('goog.events.EventHandler');31goog.require('goog.labs.net.webChannel.Channel');32goog.require('goog.labs.net.webChannel.WebChannelDebug');33goog.require('goog.labs.net.webChannel.requestStats');34goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');35goog.require('goog.labs.net.webChannel.requestStats.Stat');36goog.require('goog.net.ErrorCode');37goog.require('goog.net.EventType');38goog.require('goog.net.XmlHttp');39goog.require('goog.object');40goog.require('goog.userAgent');41424344/**45* A new ChannelRequest is created for each request to the server.46*47* @param {goog.labs.net.webChannel.Channel} channel48* The channel that owns this request.49* @param {goog.labs.net.webChannel.WebChannelDebug} channelDebug A50* WebChannelDebug to use for logging.51* @param {string=} opt_sessionId The session id for the channel.52* @param {string|number=} opt_requestId The request id for this request.53* @param {number=} opt_retryId The retry id for this request.54* @constructor55* @struct56* @final57*/58goog.labs.net.webChannel.ChannelRequest = function(59channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {60/**61* The channel object that owns the request.62* @private {goog.labs.net.webChannel.Channel}63*/64this.channel_ = channel;6566/**67* The channel debug to use for logging68* @private {goog.labs.net.webChannel.WebChannelDebug}69*/70this.channelDebug_ = channelDebug;7172/**73* The Session ID for the channel.74* @private {string|undefined}75*/76this.sid_ = opt_sessionId;7778/**79* The RID (request ID) for the request.80* @private {string|number|undefined}81*/82this.rid_ = opt_requestId;8384/**85* The attempt number of the current request.86* @private {number}87*/88this.retryId_ = opt_retryId || 1;8990/**91* An object to keep track of the channel request event listeners.92* @private {!goog.events.EventHandler<93* !goog.labs.net.webChannel.ChannelRequest>}94*/95this.eventHandler_ = new goog.events.EventHandler(this);9697/**98* The timeout in ms before failing the request.99* @private {number}100*/101this.timeout_ = goog.labs.net.webChannel.ChannelRequest.TIMEOUT_MS_;102103/**104* A timer for polling responseText in browsers that don't fire105* onreadystatechange during incremental loading of responseText.106* @private {goog.Timer}107*/108this.pollingTimer_ = new goog.Timer();109110this.pollingTimer_.setInterval(111goog.labs.net.webChannel.ChannelRequest.POLLING_INTERVAL_MS_);112113/**114* Extra HTTP headers to add to all the requests sent to the server.115* @private {Object}116*/117this.extraHeaders_ = null;118119120/**121* Whether the request was successful. This is only set to true after the122* request successfully completes.123* @private {boolean}124*/125this.successful_ = false;126127128/**129* The TimerID of the timer used to detect if the request has timed-out.130* @type {?number}131* @private132*/133this.watchDogTimerId_ = null;134135/**136* The time in the future when the request will timeout.137* @private {?number}138*/139this.watchDogTimeoutTime_ = null;140141/**142* The time the request started.143* @private {?number}144*/145this.requestStartTime_ = null;146147/**148* The type of request (XMLHTTP, IMG)149* @private {?number}150*/151this.type_ = null;152153/**154* The base Uri for the request. The includes all the parameters except the155* one that indicates the retry number.156* @private {goog.Uri}157*/158this.baseUri_ = null;159160/**161* The request Uri that was actually used for the most recent request attempt.162* @private {goog.Uri}163*/164this.requestUri_ = null;165166/**167* The post data, if the request is a post.168* @private {?string}169*/170this.postData_ = null;171172/**173* The XhrLte request if the request is using XMLHTTP174* @private {goog.net.XhrIo}175*/176this.xmlHttp_ = null;177178/**179* The position of where the next unprocessed chunk starts in the response180* text.181* @private {number}182*/183this.xmlHttpChunkStart_ = 0;184185/**186* The verb (Get or Post) for the request.187* @private {?string}188*/189this.verb_ = null;190191/**192* The last error if the request failed.193* @private {?goog.labs.net.webChannel.ChannelRequest.Error}194*/195this.lastError_ = null;196197/**198* The last status code received.199* @private {number}200*/201this.lastStatusCode_ = -1;202203/**204* Whether to send the Connection:close header as part of the request.205* @private {boolean}206*/207this.sendClose_ = true;208209/**210* Whether the request has been cancelled due to a call to cancel.211* @private {boolean}212*/213this.cancelled_ = false;214215/**216* A throttle time in ms for readystatechange events for the backchannel.217* Useful for throttling when ready state is INTERACTIVE (partial data).218* If set to zero no throttle is used.219*220* See WebChannelBase.prototype.readyStateChangeThrottleMs_221*222* @private {number}223*/224this.readyStateChangeThrottleMs_ = 0;225226/**227* The throttle for readystatechange events for the current request, or null228* if there is none.229* @private {goog.async.Throttle}230*/231this.readyStateChangeThrottle_ = null;232233234/**235* Whether to the result is expected to be encoded for chunking and thus236* requires decoding.237* @private {boolean}238*/239this.decodeChunks_ = false;240};241242243goog.scope(function() {244var Channel = goog.labs.net.webChannel.Channel;245var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;246var requestStats = goog.labs.net.webChannel.requestStats;247var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;248249250/**251* Default timeout in MS for a request. The server must return data within this252* time limit for the request to not timeout.253* @private {number}254*/255ChannelRequest.TIMEOUT_MS_ = 45 * 1000;256257258/**259* How often to poll (in MS) for changes to responseText in browsers that don't260* fire onreadystatechange during incremental loading of responseText.261* @private {number}262*/263ChannelRequest.POLLING_INTERVAL_MS_ = 250;264265266/**267* Enum for channel requests type268* @enum {number}269* @private270*/271ChannelRequest.Type_ = {272/**273* XMLHTTP requests.274*/275XML_HTTP: 1,276277/**278* IMG requests.279*/280CLOSE_REQUEST: 2281};282283284/**285* Enum type for identifying an error.286* @enum {number}287*/288ChannelRequest.Error = {289/**290* Errors due to a non-200 status code.291*/292STATUS: 0,293294/**295* Errors due to no data being returned.296*/297NO_DATA: 1,298299/**300* Errors due to a timeout.301*/302TIMEOUT: 2,303304/**305* Errors due to the server returning an unknown.306*/307UNKNOWN_SESSION_ID: 3,308309/**310* Errors due to bad data being received.311*/312BAD_DATA: 4,313314/**315* Errors due to the handler throwing an exception.316*/317HANDLER_EXCEPTION: 5,318319/**320* The browser declared itself offline during the request.321*/322BROWSER_OFFLINE: 6323};324325326/**327* Returns a useful error string for debugging based on the specified error328* code.329* @param {?ChannelRequest.Error} errorCode The error code.330* @param {number} statusCode The HTTP status code.331* @return {string} The error string for the given code combination.332*/333ChannelRequest.errorStringFromCode = function(errorCode, statusCode) {334switch (errorCode) {335case ChannelRequest.Error.STATUS:336return 'Non-200 return code (' + statusCode + ')';337case ChannelRequest.Error.NO_DATA:338return 'XMLHTTP failure (no data)';339case ChannelRequest.Error.TIMEOUT:340return 'HttpConnection timeout';341default:342return 'Unknown error';343}344};345346347/**348* Sentinel value used to indicate an invalid chunk in a multi-chunk response.349* @private {Object}350*/351ChannelRequest.INVALID_CHUNK_ = {};352353354/**355* Sentinel value used to indicate an incomplete chunk in a multi-chunk356* response.357* @private {Object}358*/359ChannelRequest.INCOMPLETE_CHUNK_ = {};360361362/**363* Returns whether XHR streaming is supported on this browser.364*365* @return {boolean} Whether XHR streaming is supported.366* @see http://code.google.com/p/closure-library/issues/detail?id=346367*/368ChannelRequest.supportsXhrStreaming = function() {369return !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(10);370};371372373/**374* Sets extra HTTP headers to add to all the requests sent to the server.375*376* @param {Object} extraHeaders The HTTP headers.377*/378ChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {379this.extraHeaders_ = extraHeaders;380};381382383/**384* Sets the timeout for a request385*386* @param {number} timeout The timeout in MS for when we fail the request.387*/388ChannelRequest.prototype.setTimeout = function(timeout) {389this.timeout_ = timeout;390};391392393/**394* Sets the throttle for handling onreadystatechange events for the request.395*396* @param {number} throttle The throttle in ms. A value of zero indicates397* no throttle.398*/399ChannelRequest.prototype.setReadyStateChangeThrottle = function(throttle) {400this.readyStateChangeThrottleMs_ = throttle;401};402403404/**405* Uses XMLHTTP to send an HTTP POST to the server.406*407* @param {goog.Uri} uri The uri of the request.408* @param {string} postData The data for the post body.409* @param {boolean} decodeChunks Whether to the result is expected to be410* encoded for chunking and thus requires decoding.411*/412ChannelRequest.prototype.xmlHttpPost = function(uri, postData, decodeChunks) {413this.type_ = ChannelRequest.Type_.XML_HTTP;414this.baseUri_ = uri.clone().makeUnique();415this.postData_ = postData;416this.decodeChunks_ = decodeChunks;417this.sendXmlHttp_(null /* hostPrefix */);418};419420421/**422* Uses XMLHTTP to send an HTTP GET to the server.423*424* @param {goog.Uri} uri The uri of the request.425* @param {boolean} decodeChunks Whether to the result is expected to be426* encoded for chunking and thus requires decoding.427* @param {?string} hostPrefix The host prefix, if we might be using a428* secondary domain. Note that it should also be in the URL, adding this429* won't cause it to be added to the URL.430* @param {boolean=} opt_noClose Whether to request that the tcp/ip connection431* should be closed.432*/433ChannelRequest.prototype.xmlHttpGet = function(434uri, decodeChunks, hostPrefix, opt_noClose) {435this.type_ = ChannelRequest.Type_.XML_HTTP;436this.baseUri_ = uri.clone().makeUnique();437this.postData_ = null;438this.decodeChunks_ = decodeChunks;439if (opt_noClose) {440this.sendClose_ = false;441}442443this.sendXmlHttp_(hostPrefix);444};445446447/**448* Sends a request via XMLHTTP according to the current state of the request449* object.450*451* @param {?string} hostPrefix The host prefix, if we might be using a secondary452* domain.453* @private454*/455ChannelRequest.prototype.sendXmlHttp_ = function(hostPrefix) {456this.requestStartTime_ = goog.now();457this.ensureWatchDogTimer_();458459// clone the base URI to create the request URI. The request uri has the460// attempt number as a parameter which helps in debugging.461this.requestUri_ = this.baseUri_.clone();462this.requestUri_.setParameterValues('t', this.retryId_);463464// send the request either as a POST or GET465this.xmlHttpChunkStart_ = 0;466var useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();467this.xmlHttp_ =468this.channel_.createXhrIo(useSecondaryDomains ? hostPrefix : null);469470if (this.readyStateChangeThrottleMs_ > 0) {471this.readyStateChangeThrottle_ = new goog.async.Throttle(472goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),473this.readyStateChangeThrottleMs_);474}475476this.eventHandler_.listen(477this.xmlHttp_, goog.net.EventType.READY_STATE_CHANGE,478this.readyStateChangeHandler_);479480var headers = this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};481if (this.postData_) {482this.verb_ = 'POST';483headers['Content-Type'] = 'application/x-www-form-urlencoded';484this.xmlHttp_.send(this.requestUri_, this.verb_, this.postData_, headers);485} else {486this.verb_ = 'GET';487488// If the user agent is webkit, we cannot send the close header since it is489// disallowed by the browser. If we attempt to set the "Connection: close"490// header in WEBKIT browser, it will actually causes an error message.491if (this.sendClose_ && !goog.userAgent.WEBKIT) {492headers['Connection'] = 'close';493}494this.xmlHttp_.send(this.requestUri_, this.verb_, null, headers);495}496requestStats.notifyServerReachabilityEvent(497requestStats.ServerReachability.REQUEST_MADE);498this.channelDebug_.xmlHttpChannelRequest(499this.verb_, this.requestUri_, this.rid_, this.retryId_, this.postData_);500};501502503/**504* Handles a readystatechange event.505* @param {goog.events.Event} evt The event.506* @private507*/508ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {509var xhr = /** @type {goog.net.XhrIo} */ (evt.target);510var throttle = this.readyStateChangeThrottle_;511if (throttle &&512xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {513// Only throttle in the partial data case.514this.channelDebug_.debug('Throttling readystatechange.');515throttle.fire();516} else {517// If we haven't throttled, just handle response directly.518this.xmlHttpHandler_(xhr);519}520};521522523/**524* XmlHttp handler525* @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.526* @private527*/528ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {529requestStats.onStartExecution();530531532try {533if (xmlhttp == this.xmlHttp_) {534this.onXmlHttpReadyStateChanged_();535} else {536this.channelDebug_.warning(537'Called back with an ' +538'unexpected xmlhttp');539}540} catch (ex) {541this.channelDebug_.debug('Failed call to OnXmlHttpReadyStateChanged_');542if (this.xmlHttp_ && this.xmlHttp_.getResponseText()) {543this.channelDebug_.dumpException(544ex, 'ResponseText: ' + this.xmlHttp_.getResponseText());545} else {546this.channelDebug_.dumpException(ex, 'No response text');547}548} finally {549requestStats.onEndExecution();550}551};552553554/**555* Called by the readystate handler for XMLHTTP requests.556*557* @private558*/559ChannelRequest.prototype.onXmlHttpReadyStateChanged_ = function() {560var readyState = this.xmlHttp_.getReadyState();561var errorCode = this.xmlHttp_.getLastErrorCode();562var statusCode = this.xmlHttp_.getStatus();563564// we get partial results in browsers that support ready state interactive.565// We also make sure that getResponseText is not null in interactive mode566// before we continue. However, we don't do it in Opera because it only567// fire readyState == INTERACTIVE once. We need the following code to poll568if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||569readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&570!goog.userAgent.OPERA && !this.xmlHttp_.getResponseText()) {571// not yet ready572return;573}574575// Dispatch any appropriate network events.576if (!this.cancelled_ && readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&577errorCode != goog.net.ErrorCode.ABORT) {578// Pretty conservative, these are the only known scenarios which we'd579// consider indicative of a truly non-functional network connection.580if (errorCode == goog.net.ErrorCode.TIMEOUT || statusCode <= 0) {581requestStats.notifyServerReachabilityEvent(582requestStats.ServerReachability.REQUEST_FAILED);583} else {584requestStats.notifyServerReachabilityEvent(585requestStats.ServerReachability.REQUEST_SUCCEEDED);586}587}588589// got some data so cancel the watchdog timer590this.cancelWatchDogTimer_();591592var status = this.xmlHttp_.getStatus();593this.lastStatusCode_ = status;594var responseText = this.xmlHttp_.getResponseText();595if (!responseText) {596this.channelDebug_.debug(597'No response text for uri ' + this.requestUri_ + ' status ' + status);598}599this.successful_ = (status == 200);600601this.channelDebug_.xmlHttpChannelResponseMetaData(602/** @type {string} */ (this.verb_), this.requestUri_, this.rid_,603this.retryId_, readyState, status);604605if (!this.successful_) {606if (status == 400 && responseText.indexOf('Unknown SID') > 0) {607// the server error string will include 'Unknown SID' which indicates the608// server doesn't know about the session (maybe it got restarted, maybe609// the user got moved to another server, etc.,). Handlers can special610// case this error611this.lastError_ = ChannelRequest.Error.UNKNOWN_SESSION_ID;612requestStats.notifyStatEvent(613requestStats.Stat.REQUEST_UNKNOWN_SESSION_ID);614this.channelDebug_.warning('XMLHTTP Unknown SID (' + this.rid_ + ')');615} else {616this.lastError_ = ChannelRequest.Error.STATUS;617requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_STATUS);618this.channelDebug_.warning(619'XMLHTTP Bad status ' + status + ' (' + this.rid_ + ')');620}621this.cleanup_();622this.dispatchFailure_();623return;624}625626if (this.decodeChunks_) {627this.decodeNextChunks_(readyState, responseText);628if (goog.userAgent.OPERA && this.successful_ &&629readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE) {630this.startPolling_();631}632} else {633this.channelDebug_.xmlHttpChannelResponseText(634this.rid_, responseText, null);635this.safeOnRequestData_(responseText);636}637638if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {639this.cleanup_();640}641642if (!this.successful_) {643return;644}645646if (!this.cancelled_) {647if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {648this.channel_.onRequestComplete(this);649} else {650// The default is false, the result from this callback shouldn't carry651// over to the next callback, otherwise the request looks successful if652// the watchdog timer gets called653this.successful_ = false;654this.ensureWatchDogTimer_();655}656}657};658659660/**661* Decodes the next set of available chunks in the response.662* @param {number} readyState The value of readyState.663* @param {string} responseText The value of responseText.664* @private665*/666ChannelRequest.prototype.decodeNextChunks_ = function(667readyState, responseText) {668var decodeNextChunksSuccessful = true;669while (!this.cancelled_ && this.xmlHttpChunkStart_ < responseText.length) {670var chunkText = this.getNextChunk_(responseText);671if (chunkText == ChannelRequest.INCOMPLETE_CHUNK_) {672if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {673// should have consumed entire response when the request is done674this.lastError_ = ChannelRequest.Error.BAD_DATA;675requestStats.notifyStatEvent(requestStats.Stat.REQUEST_INCOMPLETE_DATA);676decodeNextChunksSuccessful = false;677}678this.channelDebug_.xmlHttpChannelResponseText(679this.rid_, null, '[Incomplete Response]');680break;681} else if (chunkText == ChannelRequest.INVALID_CHUNK_) {682this.lastError_ = ChannelRequest.Error.BAD_DATA;683requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_DATA);684this.channelDebug_.xmlHttpChannelResponseText(685this.rid_, responseText, '[Invalid Chunk]');686decodeNextChunksSuccessful = false;687break;688} else {689this.channelDebug_.xmlHttpChannelResponseText(690this.rid_, /** @type {string} */ (chunkText), null);691this.safeOnRequestData_(/** @type {string} */ (chunkText));692}693}694if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&695responseText.length == 0) {696// also an error if we didn't get any response697this.lastError_ = ChannelRequest.Error.NO_DATA;698requestStats.notifyStatEvent(requestStats.Stat.REQUEST_NO_DATA);699decodeNextChunksSuccessful = false;700}701this.successful_ = this.successful_ && decodeNextChunksSuccessful;702if (!decodeNextChunksSuccessful) {703// malformed response - we make this trigger retry logic704this.channelDebug_.xmlHttpChannelResponseText(705this.rid_, responseText, '[Invalid Chunked Response]');706this.cleanup_();707this.dispatchFailure_();708}709};710711712/**713* Polls the response for new data.714* @private715*/716ChannelRequest.prototype.pollResponse_ = function() {717var readyState = this.xmlHttp_.getReadyState();718var responseText = this.xmlHttp_.getResponseText();719if (this.xmlHttpChunkStart_ < responseText.length) {720this.cancelWatchDogTimer_();721this.decodeNextChunks_(readyState, responseText);722if (this.successful_ &&723readyState != goog.net.XmlHttp.ReadyState.COMPLETE) {724this.ensureWatchDogTimer_();725}726}727};728729730/**731* Starts a polling interval for changes to responseText of the732* XMLHttpRequest, for browsers that don't fire onreadystatechange733* as data comes in incrementally. This timer is disabled in734* cleanup_().735* @private736*/737ChannelRequest.prototype.startPolling_ = function() {738this.eventHandler_.listen(739this.pollingTimer_, goog.Timer.TICK, this.pollResponse_);740this.pollingTimer_.start();741};742743744/**745* Returns the next chunk of a chunk-encoded response. This is not standard746* HTTP chunked encoding because browsers don't expose the chunk boundaries to747* the application through XMLHTTP. So we have an additional chunk encoding at748* the application level that lets us tell where the beginning and end of749* individual responses are so that we can only try to eval a complete JS array.750*751* The encoding is the size of the chunk encoded as a decimal string followed752* by a newline followed by the data.753*754* @param {string} responseText The response text from the XMLHTTP response.755* @return {string|Object} The next chunk string or a sentinel object756* indicating a special condition.757* @private758*/759ChannelRequest.prototype.getNextChunk_ = function(responseText) {760var sizeStartIndex = this.xmlHttpChunkStart_;761var sizeEndIndex = responseText.indexOf('\n', sizeStartIndex);762if (sizeEndIndex == -1) {763return ChannelRequest.INCOMPLETE_CHUNK_;764}765766var sizeAsString = responseText.substring(sizeStartIndex, sizeEndIndex);767var size = Number(sizeAsString);768if (isNaN(size)) {769return ChannelRequest.INVALID_CHUNK_;770}771772var chunkStartIndex = sizeEndIndex + 1;773if (chunkStartIndex + size > responseText.length) {774return ChannelRequest.INCOMPLETE_CHUNK_;775}776777var chunkText = responseText.substr(chunkStartIndex, size);778this.xmlHttpChunkStart_ = chunkStartIndex + size;779return chunkText;780};781782783/**784* Uses an IMG tag or navigator.sendBeacon to send an HTTP get to the server.785*786* This is only currently used to terminate the connection, as an IMG tag is787* the most reliable way to send something to the server while the page788* is getting torn down.789*790* Navigator.sendBeacon is available on Chrome and Firefox as a formal791* solution to ensure delivery without blocking window close. See792* https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon793*794* For Chrome Apps, sendBeacon is always necessary due to Content Security795* Policy (CSP) violation of using an IMG tag.796*797* @param {goog.Uri} uri The uri to send a request to.798*/799ChannelRequest.prototype.sendCloseRequest = function(uri) {800this.type_ = ChannelRequest.Type_.CLOSE_REQUEST;801this.baseUri_ = uri.clone().makeUnique();802803var requestSent = false;804805if (goog.global.navigator && goog.global.navigator.sendBeacon) {806// empty string body to avoid 413 error on chrome < 41807requestSent =808goog.global.navigator.sendBeacon(this.baseUri_.toString(), '');809}810811if (!requestSent) {812var eltImg = new Image();813eltImg.src = this.baseUri_;814}815816this.requestStartTime_ = goog.now();817this.ensureWatchDogTimer_();818};819820821/**822* Cancels the request no matter what the underlying transport is.823*/824ChannelRequest.prototype.cancel = function() {825this.cancelled_ = true;826this.cleanup_();827};828829830/**831* Ensures that there is watchdog timeout which is used to ensure that832* the connection completes in time.833*834* @private835*/836ChannelRequest.prototype.ensureWatchDogTimer_ = function() {837this.watchDogTimeoutTime_ = goog.now() + this.timeout_;838this.startWatchDogTimer_(this.timeout_);839};840841842/**843* Starts the watchdog timer which is used to ensure that the connection844* completes in time.845* @param {number} time The number of milliseconds to wait.846* @private847*/848ChannelRequest.prototype.startWatchDogTimer_ = function(time) {849if (this.watchDogTimerId_ != null) {850// assertion851throw Error('WatchDog timer not null');852}853this.watchDogTimerId_ =854requestStats.setTimeout(goog.bind(this.onWatchDogTimeout_, this), time);855};856857858/**859* Cancels the watchdog timer if it has been started.860*861* @private862*/863ChannelRequest.prototype.cancelWatchDogTimer_ = function() {864if (this.watchDogTimerId_) {865goog.global.clearTimeout(this.watchDogTimerId_);866this.watchDogTimerId_ = null;867}868};869870871/**872* Called when the watchdog timer is triggered. It also handles a case where it873* is called too early which we suspect may be happening sometimes874* (not sure why)875*876* @private877*/878ChannelRequest.prototype.onWatchDogTimeout_ = function() {879this.watchDogTimerId_ = null;880var now = goog.now();881if (now - this.watchDogTimeoutTime_ >= 0) {882this.handleTimeout_();883} else {884// got called too early for some reason885this.channelDebug_.warning('WatchDog timer called too early');886this.startWatchDogTimer_(this.watchDogTimeoutTime_ - now);887}888};889890891/**892* Called when the request has actually timed out. Will cleanup and notify the893* channel of the failure.894*895* @private896*/897ChannelRequest.prototype.handleTimeout_ = function() {898if (this.successful_) {899// Should never happen.900this.channelDebug_.severe(901'Received watchdog timeout even though request loaded successfully');902}903904this.channelDebug_.timeoutResponse(this.requestUri_);905906// IMG or SendBeacon requests never notice if they were successful,907// and always 'time out'. This fact says nothing about reachability.908if (this.type_ != ChannelRequest.Type_.CLOSE_REQUEST) {909requestStats.notifyServerReachabilityEvent(910requestStats.ServerReachability.REQUEST_FAILED);911requestStats.notifyStatEvent(requestStats.Stat.REQUEST_TIMEOUT);912}913914this.cleanup_();915916// Set error and dispatch failure.917// This is called for CLOSE_REQUEST too to ensure channel_.onRequestComplete.918this.lastError_ = ChannelRequest.Error.TIMEOUT;919this.dispatchFailure_();920};921922923/**924* Notifies the channel that this request failed.925* @private926*/927ChannelRequest.prototype.dispatchFailure_ = function() {928if (this.channel_.isClosed() || this.cancelled_) {929return;930}931932this.channel_.onRequestComplete(this);933};934935936/**937* Cleans up the objects used to make the request. This function is938* idempotent.939*940* @private941*/942ChannelRequest.prototype.cleanup_ = function() {943this.cancelWatchDogTimer_();944945goog.dispose(this.readyStateChangeThrottle_);946this.readyStateChangeThrottle_ = null;947948// Stop the polling timer, if necessary.949this.pollingTimer_.stop();950951// Unhook all event handlers.952this.eventHandler_.removeAll();953954if (this.xmlHttp_) {955// clear out this.xmlHttp_ before aborting so we handle getting reentered956// inside abort957var xmlhttp = this.xmlHttp_;958this.xmlHttp_ = null;959xmlhttp.abort();960xmlhttp.dispose();961}962};963964965/**966* Indicates whether the request was successful. Only valid after the handler967* is called to indicate completion of the request.968*969* @return {boolean} True if the request succeeded.970*/971ChannelRequest.prototype.getSuccess = function() {972return this.successful_;973};974975976/**977* If the request was not successful, returns the reason.978*979* @return {?ChannelRequest.Error} The last error.980*/981ChannelRequest.prototype.getLastError = function() {982return this.lastError_;983};984985986/**987* Returns the status code of the last request.988* @return {number} The status code of the last request.989*/990ChannelRequest.prototype.getLastStatusCode = function() {991return this.lastStatusCode_;992};993994995/**996* Returns the session id for this channel.997*998* @return {string|undefined} The session ID.999*/1000ChannelRequest.prototype.getSessionId = function() {1001return this.sid_;1002};100310041005/**1006* Returns the request id for this request. Each request has a unique request1007* id and the request IDs are a sequential increasing count.1008*1009* @return {string|number|undefined} The request ID.1010*/1011ChannelRequest.prototype.getRequestId = function() {1012return this.rid_;1013};101410151016/**1017* Returns the data for a post, if this request is a post.1018*1019* @return {?string} The POST data provided by the request initiator.1020*/1021ChannelRequest.prototype.getPostData = function() {1022return this.postData_;1023};102410251026/**1027* Returns the XhrIo request object.1028*1029* @return {?goog.net.XhrIo} Any XhrIo request created for this object.1030*/1031ChannelRequest.prototype.getXhr = function() {1032return this.xmlHttp_;1033};103410351036/**1037* Returns the time that the request started, if it has started.1038*1039* @return {?number} The time the request started, as returned by goog.now().1040*/1041ChannelRequest.prototype.getRequestStartTime = function() {1042return this.requestStartTime_;1043};104410451046/**1047* Helper to call the callback's onRequestData, which catches any1048* exception and cleans up the request.1049* @param {string} data The request data.1050* @private1051*/1052ChannelRequest.prototype.safeOnRequestData_ = function(data) {10531054try {1055this.channel_.onRequestData(this, data);1056var stats = requestStats.ServerReachability;1057requestStats.notifyServerReachabilityEvent(stats.BACK_CHANNEL_ACTIVITY);1058} catch (e) {1059// Dump debug info, but keep going without closing the channel.1060this.channelDebug_.dumpException(e, 'Error in httprequest callback');1061}1062};106310641065/**1066* Convenience factory method.1067*1068* @param {Channel} channel The channel object that owns this request.1069* @param {WebChannelDebug} channelDebug A WebChannelDebug to use for logging.1070* @param {string=} opt_sessionId The session id for the channel.1071* @param {string|number=} opt_requestId The request id for this request.1072* @param {number=} opt_retryId The retry id for this request.1073* @return {!ChannelRequest} The created channel request.1074*/1075ChannelRequest.createChannelRequest = function(1076channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {1077return new ChannelRequest(1078channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId);1079};1080}); // goog.scope108110821083