blob: 8a60a8e22886b95079dbbee4bbe4b84c949d0279 [file] [log] [blame]
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Definition of the ChannelRequest class. The ChannelRequest
* object encapsulates the logic for making a single request, either for the
* forward channel, back channel, or test channel, to the server. It contains
* the logic for the three types of transports we use in the BrowserChannel:
* XMLHTTP, Trident ActiveX (ie only), and Image request. It provides timeout
* detection. This class is part of the BrowserChannel implementation and is not
* for use by normal application code.
*
*/
goog.provide('goog.net.ChannelRequest');
goog.provide('goog.net.ChannelRequest.Error');
goog.require('goog.Timer');
goog.require('goog.async.Throttle');
goog.require('goog.events.EventHandler');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.XmlHttp');
goog.require('goog.object');
goog.require('goog.userAgent');
// TODO(nnaze): This file depends on goog.net.BrowserChannel and vice versa (a
// circular dependency). Usages of BrowserChannel are marked as
// "missingRequire" below for now. This should be fixed through refactoring.
/**
* Creates a ChannelRequest object which encapsulates a request to the server.
* A new ChannelRequest is created for each request to the server.
*
* @param {goog.net.BrowserChannel|goog.net.BrowserTestChannel} channel
* The BrowserChannel that owns this request.
* @param {goog.net.ChannelDebug} channelDebug A ChannelDebug to use for
* logging.
* @param {string=} opt_sessionId The session id for the channel.
* @param {string|number=} opt_requestId The request id for this request.
* @param {number=} opt_retryId The retry id for this request.
* @constructor
*/
goog.net.ChannelRequest = function(channel, channelDebug, opt_sessionId,
opt_requestId, opt_retryId) {
/**
* The BrowserChannel object that owns the request.
* @type {goog.net.BrowserChannel|goog.net.BrowserTestChannel}
* @private
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @type {goog.net.ChannelDebug}
* @private
*/
this.channelDebug_ = channelDebug;
/**
* The Session ID for the channel.
* @type {string|undefined}
* @private
*/
this.sid_ = opt_sessionId;
/**
* The RID (request ID) for the request.
* @type {string|number|undefined}
* @private
*/
this.rid_ = opt_requestId;
/**
* The attempt number of the current request.
* @type {number}
* @private
*/
this.retryId_ = opt_retryId || 1;
/**
* The timeout in ms before failing the request.
* @type {number}
* @private
*/
this.timeout_ = goog.net.ChannelRequest.TIMEOUT_MS;
/**
* An object to keep track of the channel request event listeners.
* @type {!goog.events.EventHandler<!goog.net.ChannelRequest>}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* A timer for polling responseText in browsers that don't fire
* onreadystatechange during incremental loading of responseText.
* @type {goog.Timer}
* @private
*/
this.pollingTimer_ = new goog.Timer();
this.pollingTimer_.setInterval(goog.net.ChannelRequest.POLLING_INTERVAL_MS);
};
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @type {Object}
* @private
*/
goog.net.ChannelRequest.prototype.extraHeaders_ = null;
/**
* Whether the request was successful. This is only set to true after the
* request successfuly completes.
* @type {boolean}
* @private
*/
goog.net.ChannelRequest.prototype.successful_ = false;
/**
* The TimerID of the timer used to detect if the request has timed-out.
* @type {?number}
* @private
*/
goog.net.ChannelRequest.prototype.watchDogTimerId_ = null;
/**
* The time in the future when the request will timeout.
* @type {?number}
* @private
*/
goog.net.ChannelRequest.prototype.watchDogTimeoutTime_ = null;
/**
* The time the request started.
* @type {?number}
* @private
*/
goog.net.ChannelRequest.prototype.requestStartTime_ = null;
/**
* The type of request (XMLHTTP, IMG, Trident)
* @type {?number}
* @private
*/
goog.net.ChannelRequest.prototype.type_ = null;
/**
* The base Uri for the request. The includes all the parameters except the
* one that indicates the retry number.
* @type {goog.Uri?}
* @private
*/
goog.net.ChannelRequest.prototype.baseUri_ = null;
/**
* The request Uri that was actually used for the most recent request attempt.
* @type {goog.Uri?}
* @private
*/
goog.net.ChannelRequest.prototype.requestUri_ = null;
/**
* The post data, if the request is a post.
* @type {?string}
* @private
*/
goog.net.ChannelRequest.prototype.postData_ = null;
/**
* The XhrLte request if the request is using XMLHTTP
* @type {goog.net.XhrIo}
* @private
*/
goog.net.ChannelRequest.prototype.xmlHttp_ = null;
/**
* The position of where the next unprocessed chunk starts in the response
* text.
* @type {number}
* @private
*/
goog.net.ChannelRequest.prototype.xmlHttpChunkStart_ = 0;
/**
* The Trident instance if the request is using Trident.
* @type {ActiveXObject}
* @private
*/
goog.net.ChannelRequest.prototype.trident_ = null;
/**
* The verb (Get or Post) for the request.
* @type {?string}
* @private
*/
goog.net.ChannelRequest.prototype.verb_ = null;
/**
* The last error if the request failed.
* @type {?goog.net.ChannelRequest.Error}
* @private
*/
goog.net.ChannelRequest.prototype.lastError_ = null;
/**
* The last status code received.
* @type {number}
* @private
*/
goog.net.ChannelRequest.prototype.lastStatusCode_ = -1;
/**
* Whether to send the Connection:close header as part of the request.
* @type {boolean}
* @private
*/
goog.net.ChannelRequest.prototype.sendClose_ = true;
/**
* Whether the request has been cancelled due to a call to cancel.
* @type {boolean}
* @private
*/
goog.net.ChannelRequest.prototype.cancelled_ = false;
/**
* A throttle time in ms for readystatechange events for the backchannel.
* Useful for throttling when ready state is INTERACTIVE (partial data).
* If set to zero no throttle is used.
*
* @see goog.net.BrowserChannel.prototype.readyStateChangeThrottleMs_
*
* @type {number}
* @private
*/
goog.net.ChannelRequest.prototype.readyStateChangeThrottleMs_ = 0;
/**
* The throttle for readystatechange events for the current request, or null
* if there is none.
* @type {goog.async.Throttle}
* @private
*/
goog.net.ChannelRequest.prototype.readyStateChangeThrottle_ = null;
/**
* Default timeout in MS for a request. The server must return data within this
* time limit for the request to not timeout.
* @type {number}
*/
goog.net.ChannelRequest.TIMEOUT_MS = 45 * 1000;
/**
* How often to poll (in MS) for changes to responseText in browsers that don't
* fire onreadystatechange during incremental loading of responseText.
* @type {number}
*/
goog.net.ChannelRequest.POLLING_INTERVAL_MS = 250;
/**
* Minimum version of Safari that receives a non-null responseText in ready
* state interactive.
* @type {string}
* @private
*/
goog.net.ChannelRequest.MIN_WEBKIT_FOR_INTERACTIVE_ = '420+';
/**
* Enum for channel requests type
* @enum {number}
* @private
*/
goog.net.ChannelRequest.Type_ = {
/**
* XMLHTTP requests.
*/
XML_HTTP: 1,
/**
* IMG requests.
*/
IMG: 2,
/**
* Requests that use the MSHTML ActiveX control.
*/
TRIDENT: 3
};
/**
* Enum type for identifying a ChannelRequest error.
* @enum {number}
*/
goog.net.ChannelRequest.Error = {
/**
* Errors due to a non-200 status code.
*/
STATUS: 0,
/**
* Errors due to no data being returned.
*/
NO_DATA: 1,
/**
* Errors due to a timeout.
*/
TIMEOUT: 2,
/**
* Errors due to the server returning an unknown.
*/
UNKNOWN_SESSION_ID: 3,
/**
* Errors due to bad data being received.
*/
BAD_DATA: 4,
/**
* Errors due to the handler throwing an exception.
*/
HANDLER_EXCEPTION: 5,
/**
* The browser declared itself offline during the request.
*/
BROWSER_OFFLINE: 6,
/**
* IE is blocking ActiveX streaming.
*/
ACTIVE_X_BLOCKED: 7
};
/**
* Returns a useful error string for debugging based on the specified error
* code.
* @param {goog.net.ChannelRequest.Error} errorCode The error code.
* @param {number} statusCode The HTTP status code.
* @return {string} The error string for the given code combination.
*/
goog.net.ChannelRequest.errorStringFromCode = function(errorCode, statusCode) {
switch (errorCode) {
case goog.net.ChannelRequest.Error.STATUS:
return 'Non-200 return code (' + statusCode + ')';
case goog.net.ChannelRequest.Error.NO_DATA:
return 'XMLHTTP failure (no data)';
case goog.net.ChannelRequest.Error.TIMEOUT:
return 'HttpConnection timeout';
default:
return 'Unknown error';
}
};
/**
* Sentinel value used to indicate an invalid chunk in a multi-chunk response.
* @type {Object}
* @private
*/
goog.net.ChannelRequest.INVALID_CHUNK_ = {};
/**
* Sentinel value used to indicate an incomplete chunk in a multi-chunk
* response.
* @type {Object}
* @private
*/
goog.net.ChannelRequest.INCOMPLETE_CHUNK_ = {};
/**
* Returns whether XHR streaming is supported on this browser.
*
* If XHR streaming is not supported, we will try to use an ActiveXObject
* to create a Forever IFrame.
*
* @return {boolean} Whether XHR streaming is supported.
* @see http://code.google.com/p/closure-library/issues/detail?id=346
*/
goog.net.ChannelRequest.supportsXhrStreaming = function() {
return !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(10);
};
/**
* Sets extra HTTP headers to add to all the requests sent to the server.
*
* @param {Object} extraHeaders The HTTP headers.
*/
goog.net.ChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {
this.extraHeaders_ = extraHeaders;
};
/**
* Sets the timeout for a request
*
* @param {number} timeout The timeout in MS for when we fail the request.
*/
goog.net.ChannelRequest.prototype.setTimeout = function(timeout) {
this.timeout_ = timeout;
};
/**
* Sets the throttle for handling onreadystatechange events for the request.
*
* @param {number} throttle The throttle in ms. A value of zero indicates
* no throttle.
*/
goog.net.ChannelRequest.prototype.setReadyStateChangeThrottle = function(
throttle) {
this.readyStateChangeThrottleMs_ = throttle;
};
/**
* Uses XMLHTTP to send an HTTP POST to the server.
*
* @param {goog.Uri} uri The uri of the request.
* @param {string} postData The data for the post body.
* @param {boolean} decodeChunks Whether to the result is expected to be
* encoded for chunking and thus requires decoding.
*/
goog.net.ChannelRequest.prototype.xmlHttpPost = function(uri, postData,
decodeChunks) {
this.type_ = goog.net.ChannelRequest.Type_.XML_HTTP;
this.baseUri_ = uri.clone().makeUnique();
this.postData_ = postData;
this.decodeChunks_ = decodeChunks;
this.sendXmlHttp_(null /* hostPrefix */);
};
/**
* Uses XMLHTTP to send an HTTP GET to the server.
*
* @param {goog.Uri} uri The uri of the request.
* @param {boolean} decodeChunks Whether to the result is expected to be
* encoded for chunking and thus requires decoding.
* @param {?string} hostPrefix The host prefix, if we might be using a
* secondary domain. Note that it should also be in the URL, adding this
* won't cause it to be added to the URL.
* @param {boolean=} opt_noClose Whether to request that the tcp/ip connection
* should be closed.
*/
goog.net.ChannelRequest.prototype.xmlHttpGet = function(uri, decodeChunks,
hostPrefix, opt_noClose) {
this.type_ = goog.net.ChannelRequest.Type_.XML_HTTP;
this.baseUri_ = uri.clone().makeUnique();
this.postData_ = null;
this.decodeChunks_ = decodeChunks;
if (opt_noClose) {
this.sendClose_ = false;
}
this.sendXmlHttp_(hostPrefix);
};
/**
* Sends a request via XMLHTTP according to the current state of the
* ChannelRequest object.
*
* @param {?string} hostPrefix The host prefix, if we might be using a secondary
* domain.
* @private
*/
goog.net.ChannelRequest.prototype.sendXmlHttp_ = function(hostPrefix) {
this.requestStartTime_ = goog.now();
this.ensureWatchDogTimer_();
// clone the base URI to create the request URI. The request uri has the
// attempt number as a parameter which helps in debugging.
this.requestUri_ = this.baseUri_.clone();
this.requestUri_.setParameterValues('t', this.retryId_);
// send the request either as a POST or GET
this.xmlHttpChunkStart_ = 0;
var useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();
this.xmlHttp_ = this.channel_.createXhrIo(useSecondaryDomains ?
hostPrefix : null);
if (this.readyStateChangeThrottleMs_ > 0) {
this.readyStateChangeThrottle_ = new goog.async.Throttle(
goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),
this.readyStateChangeThrottleMs_);
}
this.eventHandler_.listen(this.xmlHttp_,
goog.net.EventType.READY_STATE_CHANGE,
this.readyStateChangeHandler_);
var headers = this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};
if (this.postData_) {
// todo (jonp) - use POST constant when Dan defines it
this.verb_ = 'POST';
headers['Content-Type'] = 'application/x-www-form-urlencoded';
this.xmlHttp_.send(this.requestUri_, this.verb_, this.postData_, headers);
} else {
// todo (jonp) - use GET constant when Dan defines it
this.verb_ = 'GET';
// If the user agent is webkit, we cannot send the close header since it is
// disallowed by the browser. If we attempt to set the "Connection: close"
// header in WEBKIT browser, it will actually causes an error message.
if (this.sendClose_ && !goog.userAgent.WEBKIT) {
headers['Connection'] = 'close';
}
this.xmlHttp_.send(this.requestUri_, this.verb_, null, headers);
}
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */ (
goog.net.BrowserChannel.ServerReachability.REQUEST_MADE));
this.channelDebug_.xmlHttpChannelRequest(this.verb_,
this.requestUri_, this.rid_, this.retryId_,
this.postData_);
};
/**
* Handles a readystatechange event.
* @param {goog.events.Event} evt The event.
* @private
*/
goog.net.ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {
var xhr = /** @type {goog.net.XhrIo} */ (evt.target);
var throttle = this.readyStateChangeThrottle_;
if (throttle &&
xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
// Only throttle in the partial data case.
this.channelDebug_.debug('Throttling readystatechange.');
throttle.fire();
} else {
// If we haven't throttled, just handle response directly.
this.xmlHttpHandler_(xhr);
}
};
/**
* XmlHttp handler
* @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.
* @private
*/
goog.net.ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {
/** @suppress {missingRequire} */
goog.net.BrowserChannel.onStartExecution();
/** @preserveTry */
try {
if (xmlhttp == this.xmlHttp_) {
this.onXmlHttpReadyStateChanged_();
} else {
this.channelDebug_.warning('Called back with an ' +
'unexpected xmlhttp');
}
} catch (ex) {
this.channelDebug_.debug('Failed call to OnXmlHttpReadyStateChanged_');
if (this.xmlHttp_ && this.xmlHttp_.getResponseText()) {
this.channelDebug_.dumpException(ex,
'ResponseText: ' + this.xmlHttp_.getResponseText());
} else {
this.channelDebug_.dumpException(ex, 'No response text');
}
} finally {
/** @suppress {missingRequire} */
goog.net.BrowserChannel.onEndExecution();
}
};
/**
* Called by the readystate handler for XMLHTTP requests.
*
* @private
*/
goog.net.ChannelRequest.prototype.onXmlHttpReadyStateChanged_ = function() {
var readyState = this.xmlHttp_.getReadyState();
var errorCode = this.xmlHttp_.getLastErrorCode();
var statusCode = this.xmlHttp_.getStatus();
// If it is Safari less than 420+, there is a bug that causes null to be
// in the responseText on ready state interactive so we must wait for
// ready state complete.
if (!goog.net.ChannelRequest.supportsXhrStreaming() ||
(goog.userAgent.WEBKIT &&
!goog.userAgent.isVersionOrHigher(
goog.net.ChannelRequest.MIN_WEBKIT_FOR_INTERACTIVE_))) {
if (readyState < goog.net.XmlHttp.ReadyState.COMPLETE) {
// not yet ready
return;
}
} else {
// we get partial results in browsers that support ready state interactive.
// We also make sure that getResponseText is not null in interactive mode
// before we continue. However, we don't do it in Opera because it only
// fire readyState == INTERACTIVE once. We need the following code to poll
if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&
!goog.userAgent.OPERA && !this.xmlHttp_.getResponseText()) {
// not yet ready
return;
}
}
// Dispatch any appropriate network events.
if (!this.cancelled_ && readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
errorCode != goog.net.ErrorCode.ABORT) {
// Pretty conservative, these are the only known scenarios which we'd
// consider indicative of a truly non-functional network connection.
if (errorCode == goog.net.ErrorCode.TIMEOUT ||
statusCode <= 0) {
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.REQUEST_FAILED);
} else {
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.REQUEST_SUCCEEDED);
}
}
// got some data so cancel the watchdog timer
this.cancelWatchDogTimer_();
var status = this.xmlHttp_.getStatus();
this.lastStatusCode_ = status;
var responseText = this.xmlHttp_.getResponseText();
if (!responseText) {
this.channelDebug_.debug('No response text for uri ' +
this.requestUri_ + ' status ' + status);
}
this.successful_ = (status == 200);
this.channelDebug_.xmlHttpChannelResponseMetaData(
/** @type {string} */ (this.verb_),
this.requestUri_, this.rid_, this.retryId_, readyState,
status);
if (!this.successful_) {
if (status == 400 &&
responseText.indexOf('Unknown SID') > 0) {
// the server error string will include 'Unknown SID' which indicates the
// server doesn't know about the session (maybe it got restarted, maybe
// the user got moved to another server, etc.,). Handlers can special
// case this error
this.lastError_ = goog.net.ChannelRequest.Error.UNKNOWN_SESSION_ID;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_UNKNOWN_SESSION_ID);
this.channelDebug_.warning('XMLHTTP Unknown SID (' + this.rid_ + ')');
} else {
this.lastError_ = goog.net.ChannelRequest.Error.STATUS;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_BAD_STATUS);
this.channelDebug_.warning(
'XMLHTTP Bad status ' + status + ' (' + this.rid_ + ')');
}
this.cleanup_();
this.dispatchFailure_();
return;
}
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
this.cleanup_();
}
if (this.decodeChunks_) {
this.decodeNextChunks_(readyState, responseText);
if (goog.userAgent.OPERA && this.successful_ &&
readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
this.startPolling_();
}
} else {
this.channelDebug_.xmlHttpChannelResponseText(
this.rid_, responseText, null);
this.safeOnRequestData_(responseText);
}
if (!this.successful_) {
return;
}
if (!this.cancelled_) {
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
this.channel_.onRequestComplete(this);
} else {
// The default is false, the result from this callback shouldn't carry
// over to the next callback, otherwise the request looks successful if
// the watchdog timer gets called
this.successful_ = false;
this.ensureWatchDogTimer_();
}
}
};
/**
* Decodes the next set of available chunks in the response.
* @param {number} readyState The value of readyState.
* @param {string} responseText The value of responseText.
* @private
*/
goog.net.ChannelRequest.prototype.decodeNextChunks_ = function(readyState,
responseText) {
var decodeNextChunksSuccessful = true;
while (!this.cancelled_ &&
this.xmlHttpChunkStart_ < responseText.length) {
var chunkText = this.getNextChunk_(responseText);
if (chunkText == goog.net.ChannelRequest.INCOMPLETE_CHUNK_) {
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
// should have consumed entire response when the request is done
this.lastError_ = goog.net.ChannelRequest.Error.BAD_DATA;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_INCOMPLETE_DATA);
decodeNextChunksSuccessful = false;
}
this.channelDebug_.xmlHttpChannelResponseText(
this.rid_, null, '[Incomplete Response]');
break;
} else if (chunkText == goog.net.ChannelRequest.INVALID_CHUNK_) {
this.lastError_ = goog.net.ChannelRequest.Error.BAD_DATA;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_BAD_DATA);
this.channelDebug_.xmlHttpChannelResponseText(
this.rid_, responseText, '[Invalid Chunk]');
decodeNextChunksSuccessful = false;
break;
} else {
this.channelDebug_.xmlHttpChannelResponseText(
this.rid_, /** @type {string} */ (chunkText), null);
this.safeOnRequestData_(/** @type {string} */ (chunkText));
}
}
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
responseText.length == 0) {
// also an error if we didn't get any response
this.lastError_ = goog.net.ChannelRequest.Error.NO_DATA;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_NO_DATA);
decodeNextChunksSuccessful = false;
}
this.successful_ = this.successful_ && decodeNextChunksSuccessful;
if (!decodeNextChunksSuccessful) {
// malformed response - we make this trigger retry logic
this.channelDebug_.xmlHttpChannelResponseText(
this.rid_, responseText, '[Invalid Chunked Response]');
this.cleanup_();
this.dispatchFailure_();
}
};
/**
* Polls the response for new data.
* @private
*/
goog.net.ChannelRequest.prototype.pollResponse_ = function() {
var readyState = this.xmlHttp_.getReadyState();
var responseText = this.xmlHttp_.getResponseText();
if (this.xmlHttpChunkStart_ < responseText.length) {
this.cancelWatchDogTimer_();
this.decodeNextChunks_(readyState, responseText);
if (this.successful_ &&
readyState != goog.net.XmlHttp.ReadyState.COMPLETE) {
this.ensureWatchDogTimer_();
}
}
};
/**
* Starts a polling interval for changes to responseText of the
* XMLHttpRequest, for browsers that don't fire onreadystatechange
* as data comes in incrementally. This timer is disabled in
* cleanup_().
* @private
*/
goog.net.ChannelRequest.prototype.startPolling_ = function() {
this.eventHandler_.listen(this.pollingTimer_, goog.Timer.TICK,
this.pollResponse_);
this.pollingTimer_.start();
};
/**
* Returns the next chunk of a chunk-encoded response. This is not standard
* HTTP chunked encoding because browsers don't expose the chunk boundaries to
* the application through XMLHTTP. So we have an additional chunk encoding at
* the application level that lets us tell where the beginning and end of
* individual responses are so that we can only try to eval a complete JS array.
*
* The encoding is the size of the chunk encoded as a decimal string followed
* by a newline followed by the data.
*
* @param {string} responseText The response text from the XMLHTTP response.
* @return {string|Object} The next chunk string or a sentinel object
* indicating a special condition.
* @private
*/
goog.net.ChannelRequest.prototype.getNextChunk_ = function(responseText) {
var sizeStartIndex = this.xmlHttpChunkStart_;
var sizeEndIndex = responseText.indexOf('\n', sizeStartIndex);
if (sizeEndIndex == -1) {
return goog.net.ChannelRequest.INCOMPLETE_CHUNK_;
}
var sizeAsString = responseText.substring(sizeStartIndex, sizeEndIndex);
var size = Number(sizeAsString);
if (isNaN(size)) {
return goog.net.ChannelRequest.INVALID_CHUNK_;
}
var chunkStartIndex = sizeEndIndex + 1;
if (chunkStartIndex + size > responseText.length) {
return goog.net.ChannelRequest.INCOMPLETE_CHUNK_;
}
var chunkText = responseText.substr(chunkStartIndex, size);
this.xmlHttpChunkStart_ = chunkStartIndex + size;
return chunkText;
};
/**
* Uses the Trident htmlfile ActiveX control to send a GET request in IE. This
* is the innovation discovered that lets us get intermediate results in
* Internet Explorer. Thanks to http://go/kev
* @param {goog.Uri} uri The uri to request from.
* @param {boolean} usingSecondaryDomain Whether to use a secondary domain.
*/
goog.net.ChannelRequest.prototype.tridentGet = function(uri,
usingSecondaryDomain) {
this.type_ = goog.net.ChannelRequest.Type_.TRIDENT;
this.baseUri_ = uri.clone().makeUnique();
this.tridentGet_(usingSecondaryDomain);
};
/**
* Starts the Trident request.
* @param {boolean} usingSecondaryDomain Whether to use a secondary domain.
* @private
*/
goog.net.ChannelRequest.prototype.tridentGet_ = function(usingSecondaryDomain) {
this.requestStartTime_ = goog.now();
this.ensureWatchDogTimer_();
var hostname = usingSecondaryDomain ? window.location.hostname : '';
this.requestUri_ = this.baseUri_.clone();
this.requestUri_.setParameterValue('DOMAIN', hostname);
this.requestUri_.setParameterValue('t', this.retryId_);
try {
this.trident_ = new ActiveXObject('htmlfile');
} catch (e) {
this.channelDebug_.severe('ActiveX blocked');
this.cleanup_();
this.lastError_ = goog.net.ChannelRequest.Error.ACTIVE_X_BLOCKED;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.ACTIVE_X_BLOCKED);
this.dispatchFailure_();
return;
}
var body = '<html><body>';
if (usingSecondaryDomain) {
body += '<script>document.domain="' + hostname + '"</scr' + 'ipt>';
}
body += '</body></html>';
this.trident_.open();
this.trident_.write(body);
this.trident_.close();
this.trident_.parentWindow['m'] = goog.bind(this.onTridentRpcMessage_, this);
this.trident_.parentWindow['d'] = goog.bind(this.onTridentDone_, this, true);
this.trident_.parentWindow['rpcClose'] =
goog.bind(this.onTridentDone_, this, false);
var div = this.trident_.createElement('div');
this.trident_.parentWindow.document.body.appendChild(div);
div.innerHTML = '<iframe src="' + this.requestUri_ + '"></iframe>';
this.channelDebug_.tridentChannelRequest('GET',
this.requestUri_, this.rid_, this.retryId_);
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.REQUEST_MADE);
};
/**
* Callback from the Trident htmlfile ActiveX control for when a new message
* is received.
*
* @param {string} msg The data payload.
* @private
*/
goog.net.ChannelRequest.prototype.onTridentRpcMessage_ = function(msg) {
// need to do async b/c this gets called off of the context of the ActiveX
/** @suppress {missingRequire} */
goog.net.BrowserChannel.setTimeout(
goog.bind(this.onTridentRpcMessageAsync_, this, msg), 0);
};
/**
* Callback from the Trident htmlfile ActiveX control for when a new message
* is received.
*
* @param {string} msg The data payload.
* @private
*/
goog.net.ChannelRequest.prototype.onTridentRpcMessageAsync_ = function(msg) {
if (this.cancelled_) {
return;
}
this.channelDebug_.tridentChannelResponseText(this.rid_, msg);
this.cancelWatchDogTimer_();
this.safeOnRequestData_(msg);
this.ensureWatchDogTimer_();
};
/**
* Callback from the Trident htmlfile ActiveX control for when the request
* is complete
*
* @param {boolean} successful Whether the request successfully completed.
* @private
*/
goog.net.ChannelRequest.prototype.onTridentDone_ = function(successful) {
// need to do async b/c this gets called off of the context of the ActiveX
/** @suppress {missingRequire} */
goog.net.BrowserChannel.setTimeout(
goog.bind(this.onTridentDoneAsync_, this, successful), 0);
};
/**
* Callback from the Trident htmlfile ActiveX control for when the request
* is complete
*
* @param {boolean} successful Whether the request successfully completed.
* @private
*/
goog.net.ChannelRequest.prototype.onTridentDoneAsync_ = function(successful) {
if (this.cancelled_) {
return;
}
this.channelDebug_.tridentChannelResponseDone(
this.rid_, successful);
this.cleanup_();
this.successful_ = successful;
this.channel_.onRequestComplete(this);
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.BACK_CHANNEL_ACTIVITY);
};
/**
* Uses an IMG tag to send an HTTP get to the server. This is only currently
* used to terminate the connection, as an IMG tag is the most reliable way to
* send something to the server while the page is getting torn down.
* @param {goog.Uri} uri The uri to send a request to.
*/
goog.net.ChannelRequest.prototype.sendUsingImgTag = function(uri) {
this.type_ = goog.net.ChannelRequest.Type_.IMG;
this.baseUri_ = uri.clone().makeUnique();
this.imgTagGet_();
};
/**
* Starts the IMG request.
*
* @private
*/
goog.net.ChannelRequest.prototype.imgTagGet_ = function() {
var eltImg = new Image();
eltImg.src = this.baseUri_;
this.requestStartTime_ = goog.now();
this.ensureWatchDogTimer_();
};
/**
* Cancels the request no matter what the underlying transport is.
*/
goog.net.ChannelRequest.prototype.cancel = function() {
this.cancelled_ = true;
this.cleanup_();
};
/**
* Ensures that there is watchdog timeout which is used to ensure that
* the connection completes in time.
*
* @private
*/
goog.net.ChannelRequest.prototype.ensureWatchDogTimer_ = function() {
this.watchDogTimeoutTime_ = goog.now() + this.timeout_;
this.startWatchDogTimer_(this.timeout_);
};
/**
* Starts the watchdog timer which is used to ensure that the connection
* completes in time.
* @param {number} time The number of milliseconds to wait.
* @private
*/
goog.net.ChannelRequest.prototype.startWatchDogTimer_ = function(time) {
if (this.watchDogTimerId_ != null) {
// assertion
throw Error('WatchDog timer not null');
}
this.watchDogTimerId_ = /** @suppress {missingRequire} */ (
goog.net.BrowserChannel.setTimeout(
goog.bind(this.onWatchDogTimeout_, this), time));
};
/**
* Cancels the watchdog timer if it has been started.
*
* @private
*/
goog.net.ChannelRequest.prototype.cancelWatchDogTimer_ = function() {
if (this.watchDogTimerId_) {
goog.global.clearTimeout(this.watchDogTimerId_);
this.watchDogTimerId_ = null;
}
};
/**
* Called when the watchdog timer is triggered. It also handles a case where it
* is called too early which we suspect may be happening sometimes
* (not sure why)
*
* @private
*/
goog.net.ChannelRequest.prototype.onWatchDogTimeout_ = function() {
this.watchDogTimerId_ = null;
var now = goog.now();
if (now - this.watchDogTimeoutTime_ >= 0) {
this.handleTimeout_();
} else {
// got called too early for some reason
this.channelDebug_.warning('WatchDog timer called too early');
this.startWatchDogTimer_(this.watchDogTimeoutTime_ - now);
}
};
/**
* Called when the request has actually timed out. Will cleanup and notify the
* channel of the failure.
*
* @private
*/
goog.net.ChannelRequest.prototype.handleTimeout_ = function() {
if (this.successful_) {
// Should never happen.
this.channelDebug_.severe(
'Received watchdog timeout even though request loaded successfully');
}
this.channelDebug_.timeoutResponse(this.requestUri_);
// IMG requests never notice if they were successful, and always 'time out'.
// This fact says nothing about reachability.
if (this.type_ != goog.net.ChannelRequest.Type_.IMG) {
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.REQUEST_FAILED);
}
this.cleanup_();
// set error and dispatch failure
this.lastError_ = goog.net.ChannelRequest.Error.TIMEOUT;
/** @suppress {missingRequire} */
goog.net.BrowserChannel.notifyStatEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.Stat.REQUEST_TIMEOUT);
this.dispatchFailure_();
};
/**
* Notifies the channel that this request failed.
* @private
*/
goog.net.ChannelRequest.prototype.dispatchFailure_ = function() {
if (this.channel_.isClosed() || this.cancelled_) {
return;
}
this.channel_.onRequestComplete(this);
};
/**
* Cleans up the objects used to make the request. This function is
* idempotent.
*
* @private
*/
goog.net.ChannelRequest.prototype.cleanup_ = function() {
this.cancelWatchDogTimer_();
goog.dispose(this.readyStateChangeThrottle_);
this.readyStateChangeThrottle_ = null;
// Stop the polling timer, if necessary.
this.pollingTimer_.stop();
// Unhook all event handlers.
this.eventHandler_.removeAll();
if (this.xmlHttp_) {
// clear out this.xmlHttp_ before aborting so we handle getting reentered
// inside abort
var xmlhttp = this.xmlHttp_;
this.xmlHttp_ = null;
xmlhttp.abort();
xmlhttp.dispose();
}
if (this.trident_) {
this.trident_ = null;
}
};
/**
* Indicates whether the request was successful. Only valid after the handler
* is called to indicate completion of the request.
*
* @return {boolean} True if the request succeeded.
*/
goog.net.ChannelRequest.prototype.getSuccess = function() {
return this.successful_;
};
/**
* If the request was not successful, returns the reason.
*
* @return {?goog.net.ChannelRequest.Error} The last error.
*/
goog.net.ChannelRequest.prototype.getLastError = function() {
return this.lastError_;
};
/**
* Returns the status code of the last request.
* @return {number} The status code of the last request.
*/
goog.net.ChannelRequest.prototype.getLastStatusCode = function() {
return this.lastStatusCode_;
};
/**
* Returns the session id for this channel.
*
* @return {string|undefined} The session ID.
*/
goog.net.ChannelRequest.prototype.getSessionId = function() {
return this.sid_;
};
/**
* Returns the request id for this request. Each request has a unique request
* id and the request IDs are a sequential increasing count.
*
* @return {string|number|undefined} The request ID.
*/
goog.net.ChannelRequest.prototype.getRequestId = function() {
return this.rid_;
};
/**
* Returns the data for a post, if this request is a post.
*
* @return {?string} The POST data provided by the request initiator.
*/
goog.net.ChannelRequest.prototype.getPostData = function() {
return this.postData_;
};
/**
* Returns the time that the request started, if it has started.
*
* @return {?number} The time the request started, as returned by goog.now().
*/
goog.net.ChannelRequest.prototype.getRequestStartTime = function() {
return this.requestStartTime_;
};
/**
* Helper to call the callback's onRequestData, which catches any
* exception and cleans up the request.
* @param {string} data The request data.
* @private
*/
goog.net.ChannelRequest.prototype.safeOnRequestData_ = function(data) {
/** @preserveTry */
try {
this.channel_.onRequestData(this, data);
this.channel_.notifyServerReachabilityEvent(
/** @suppress {missingRequire} */
goog.net.BrowserChannel.ServerReachability.BACK_CHANNEL_ACTIVITY);
} catch (e) {
// Dump debug info, but keep going without closing the channel.
this.channelDebug_.dumpException(
e, 'Error in httprequest callback');
}
};