| // 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 Wrapper class for handling XmlHttpRequests. |
| * |
| * One off requests can be sent through goog.net.XhrIo.send() or an |
| * instance can be created to send multiple requests. Each request uses its |
| * own XmlHttpRequest object and handles clearing of the event callback to |
| * ensure no leaks. |
| * |
| * XhrIo is event based, it dispatches events when a request finishes, fails or |
| * succeeds or when the ready-state changes. The ready-state or timeout event |
| * fires first, followed by a generic completed event. Then the abort, error, |
| * or success event is fired as appropriate. Lastly, the ready event will fire |
| * to indicate that the object may be used to make another request. |
| * |
| * The error event may also be called before completed and |
| * ready-state-change if the XmlHttpRequest.open() or .send() methods throw. |
| * |
| * This class does not support multiple requests, queuing, or prioritization. |
| * |
| * Tested = IE6, FF1.5, Safari, Opera 8.5 |
| * |
| * TODO(user): Error cases aren't playing nicely in Safari. |
| * |
| */ |
| |
| |
| goog.provide('goog.net.XhrIo'); |
| goog.provide('goog.net.XhrIo.ResponseType'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.array'); |
| goog.require('goog.debug.entryPointRegistry'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.json'); |
| goog.require('goog.log'); |
| goog.require('goog.net.ErrorCode'); |
| goog.require('goog.net.EventType'); |
| goog.require('goog.net.HttpStatus'); |
| goog.require('goog.net.XmlHttp'); |
| goog.require('goog.object'); |
| goog.require('goog.string'); |
| goog.require('goog.structs'); |
| goog.require('goog.structs.Map'); |
| goog.require('goog.uri.utils'); |
| goog.require('goog.userAgent'); |
| |
| goog.forwardDeclare('goog.Uri'); |
| |
| |
| |
| /** |
| * Basic class for handling XMLHttpRequests. |
| * @param {goog.net.XmlHttpFactory=} opt_xmlHttpFactory Factory to use when |
| * creating XMLHttpRequest objects. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| */ |
| goog.net.XhrIo = function(opt_xmlHttpFactory) { |
| goog.net.XhrIo.base(this, 'constructor'); |
| |
| /** |
| * Map of default headers to add to every request, use: |
| * XhrIo.headers.set(name, value) |
| * @type {!goog.structs.Map} |
| */ |
| this.headers = new goog.structs.Map(); |
| |
| /** |
| * Optional XmlHttpFactory |
| * @private {goog.net.XmlHttpFactory} |
| */ |
| this.xmlHttpFactory_ = opt_xmlHttpFactory || null; |
| |
| /** |
| * Whether XMLHttpRequest is active. A request is active from the time send() |
| * is called until onReadyStateChange() is complete, or error() or abort() |
| * is called. |
| * @private {boolean} |
| */ |
| this.active_ = false; |
| |
| /** |
| * The XMLHttpRequest object that is being used for the transfer. |
| * @private {?goog.net.XhrLike.OrNative} |
| */ |
| this.xhr_ = null; |
| |
| /** |
| * The options to use with the current XMLHttpRequest object. |
| * @private {Object} |
| */ |
| this.xhrOptions_ = null; |
| |
| /** |
| * Last URL that was requested. |
| * @private {string|goog.Uri} |
| */ |
| this.lastUri_ = ''; |
| |
| /** |
| * Method for the last request. |
| * @private {string} |
| */ |
| this.lastMethod_ = ''; |
| |
| /** |
| * Last error code. |
| * @private {!goog.net.ErrorCode} |
| */ |
| this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR; |
| |
| /** |
| * Last error message. |
| * @private {Error|string} |
| */ |
| this.lastError_ = ''; |
| |
| /** |
| * Used to ensure that we don't dispatch an multiple ERROR events. This can |
| * happen in IE when it does a synchronous load and one error is handled in |
| * the ready statte change and one is handled due to send() throwing an |
| * exception. |
| * @private {boolean} |
| */ |
| this.errorDispatched_ = false; |
| |
| /** |
| * Used to make sure we don't fire the complete event from inside a send call. |
| * @private {boolean} |
| */ |
| this.inSend_ = false; |
| |
| /** |
| * Used in determining if a call to {@link #onReadyStateChange_} is from |
| * within a call to this.xhr_.open. |
| * @private {boolean} |
| */ |
| this.inOpen_ = false; |
| |
| /** |
| * Used in determining if a call to {@link #onReadyStateChange_} is from |
| * within a call to this.xhr_.abort. |
| * @private {boolean} |
| */ |
| this.inAbort_ = false; |
| |
| /** |
| * Number of milliseconds after which an incomplete request will be aborted |
| * and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no timeout |
| * is set. |
| * @private {number} |
| */ |
| this.timeoutInterval_ = 0; |
| |
| /** |
| * Timer to track request timeout. |
| * @private {?number} |
| */ |
| this.timeoutId_ = null; |
| |
| /** |
| * The requested type for the response. The empty string means use the default |
| * XHR behavior. |
| * @private {goog.net.XhrIo.ResponseType} |
| */ |
| this.responseType_ = goog.net.XhrIo.ResponseType.DEFAULT; |
| |
| /** |
| * Whether a "credentialed" request is to be sent (one that is aware of |
| * cookies and authentication). This is applicable only for cross-domain |
| * requests and more recent browsers that support this part of the HTTP Access |
| * Control standard. |
| * |
| * @see http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute |
| * |
| * @private {boolean} |
| */ |
| this.withCredentials_ = false; |
| |
| /** |
| * True if we can use XMLHttpRequest's timeout directly. |
| * @private {boolean} |
| */ |
| this.useXhr2Timeout_ = false; |
| }; |
| goog.inherits(goog.net.XhrIo, goog.events.EventTarget); |
| |
| |
| /** |
| * Response types that may be requested for XMLHttpRequests. |
| * @enum {string} |
| * @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetype-attribute |
| */ |
| goog.net.XhrIo.ResponseType = { |
| DEFAULT: '', |
| TEXT: 'text', |
| DOCUMENT: 'document', |
| // Not supported as of Chrome 10.0.612.1 dev |
| BLOB: 'blob', |
| ARRAY_BUFFER: 'arraybuffer' |
| }; |
| |
| |
| /** |
| * A reference to the XhrIo logger |
| * @private {goog.debug.Logger} |
| * @const |
| */ |
| goog.net.XhrIo.prototype.logger_ = |
| goog.log.getLogger('goog.net.XhrIo'); |
| |
| |
| /** |
| * The Content-Type HTTP header name |
| * @type {string} |
| */ |
| goog.net.XhrIo.CONTENT_TYPE_HEADER = 'Content-Type'; |
| |
| |
| /** |
| * The pattern matching the 'http' and 'https' URI schemes |
| * @type {!RegExp} |
| */ |
| goog.net.XhrIo.HTTP_SCHEME_PATTERN = /^https?$/i; |
| |
| |
| /** |
| * The methods that typically come along with form data. We set different |
| * headers depending on whether the HTTP action is one of these. |
| */ |
| goog.net.XhrIo.METHODS_WITH_FORM_DATA = ['POST', 'PUT']; |
| |
| |
| /** |
| * The Content-Type HTTP header value for a url-encoded form |
| * @type {string} |
| */ |
| goog.net.XhrIo.FORM_CONTENT_TYPE = |
| 'application/x-www-form-urlencoded;charset=utf-8'; |
| |
| |
| /** |
| * The XMLHttpRequest Level two timeout delay ms property name. |
| * |
| * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute |
| * |
| * @private {string} |
| * @const |
| */ |
| goog.net.XhrIo.XHR2_TIMEOUT_ = 'timeout'; |
| |
| |
| /** |
| * The XMLHttpRequest Level two ontimeout handler property name. |
| * |
| * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute |
| * |
| * @private {string} |
| * @const |
| */ |
| goog.net.XhrIo.XHR2_ON_TIMEOUT_ = 'ontimeout'; |
| |
| |
| /** |
| * All non-disposed instances of goog.net.XhrIo created |
| * by {@link goog.net.XhrIo.send} are in this Array. |
| * @see goog.net.XhrIo.cleanup |
| * @private {!Array<!goog.net.XhrIo>} |
| */ |
| goog.net.XhrIo.sendInstances_ = []; |
| |
| |
| /** |
| * Static send that creates a short lived instance of XhrIo to send the |
| * request. |
| * @see goog.net.XhrIo.cleanup |
| * @param {string|goog.Uri} url Uri to make request to. |
| * @param {?function(this:goog.net.XhrIo, ?)=} opt_callback Callback function |
| * for when request is complete. |
| * @param {string=} opt_method Send method, default: GET. |
| * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=} |
| * opt_content Body data. |
| * @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the |
| * request. |
| * @param {number=} opt_timeoutInterval Number of milliseconds after which an |
| * incomplete request will be aborted; 0 means no timeout is set. |
| * @param {boolean=} opt_withCredentials Whether to send credentials with the |
| * request. Default to false. See {@link goog.net.XhrIo#setWithCredentials}. |
| * @return {!goog.net.XhrIo} The sent XhrIo. |
| */ |
| goog.net.XhrIo.send = function(url, opt_callback, opt_method, opt_content, |
| opt_headers, opt_timeoutInterval, |
| opt_withCredentials) { |
| var x = new goog.net.XhrIo(); |
| goog.net.XhrIo.sendInstances_.push(x); |
| if (opt_callback) { |
| x.listen(goog.net.EventType.COMPLETE, opt_callback); |
| } |
| x.listenOnce(goog.net.EventType.READY, x.cleanupSend_); |
| if (opt_timeoutInterval) { |
| x.setTimeoutInterval(opt_timeoutInterval); |
| } |
| if (opt_withCredentials) { |
| x.setWithCredentials(opt_withCredentials); |
| } |
| x.send(url, opt_method, opt_content, opt_headers); |
| return x; |
| }; |
| |
| |
| /** |
| * Disposes all non-disposed instances of goog.net.XhrIo created by |
| * {@link goog.net.XhrIo.send}. |
| * {@link goog.net.XhrIo.send} cleans up the goog.net.XhrIo instance |
| * it creates when the request completes or fails. However, if |
| * the request never completes, then the goog.net.XhrIo is not disposed. |
| * This can occur if the window is unloaded before the request completes. |
| * We could have {@link goog.net.XhrIo.send} return the goog.net.XhrIo |
| * it creates and make the client of {@link goog.net.XhrIo.send} be |
| * responsible for disposing it in this case. However, this makes things |
| * significantly more complicated for the client, and the whole point |
| * of {@link goog.net.XhrIo.send} is that it's simple and easy to use. |
| * Clients of {@link goog.net.XhrIo.send} should call |
| * {@link goog.net.XhrIo.cleanup} when doing final |
| * cleanup on window unload. |
| */ |
| goog.net.XhrIo.cleanup = function() { |
| var instances = goog.net.XhrIo.sendInstances_; |
| while (instances.length) { |
| instances.pop().dispose(); |
| } |
| }; |
| |
| |
| /** |
| * Installs exception protection for all entry point introduced by |
| * goog.net.XhrIo instances which are not protected by |
| * {@link goog.debug.ErrorHandler#protectWindowSetTimeout}, |
| * {@link goog.debug.ErrorHandler#protectWindowSetInterval}, or |
| * {@link goog.events.protectBrowserEventEntryPoint}. |
| * |
| * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to |
| * protect the entry point(s). |
| */ |
| goog.net.XhrIo.protectEntryPoints = function(errorHandler) { |
| goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = |
| errorHandler.protectEntryPoint( |
| goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_); |
| }; |
| |
| |
| /** |
| * Disposes of the specified goog.net.XhrIo created by |
| * {@link goog.net.XhrIo.send} and removes it from |
| * {@link goog.net.XhrIo.pendingStaticSendInstances_}. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.cleanupSend_ = function() { |
| this.dispose(); |
| goog.array.remove(goog.net.XhrIo.sendInstances_, this); |
| }; |
| |
| |
| /** |
| * Returns the number of milliseconds after which an incomplete request will be |
| * aborted, or 0 if no timeout is set. |
| * @return {number} Timeout interval in milliseconds. |
| */ |
| goog.net.XhrIo.prototype.getTimeoutInterval = function() { |
| return this.timeoutInterval_; |
| }; |
| |
| |
| /** |
| * Sets the number of milliseconds after which an incomplete request will be |
| * aborted and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no |
| * timeout is set. |
| * @param {number} ms Timeout interval in milliseconds; 0 means none. |
| */ |
| goog.net.XhrIo.prototype.setTimeoutInterval = function(ms) { |
| this.timeoutInterval_ = Math.max(0, ms); |
| }; |
| |
| |
| /** |
| * Sets the desired type for the response. At time of writing, this is only |
| * supported in very recent versions of WebKit (10.0.612.1 dev and later). |
| * |
| * If this is used, the response may only be accessed via {@link #getResponse}. |
| * |
| * @param {goog.net.XhrIo.ResponseType} type The desired type for the response. |
| */ |
| goog.net.XhrIo.prototype.setResponseType = function(type) { |
| this.responseType_ = type; |
| }; |
| |
| |
| /** |
| * Gets the desired type for the response. |
| * @return {goog.net.XhrIo.ResponseType} The desired type for the response. |
| */ |
| goog.net.XhrIo.prototype.getResponseType = function() { |
| return this.responseType_; |
| }; |
| |
| |
| /** |
| * Sets whether a "credentialed" request that is aware of cookie and |
| * authentication information should be made. This option is only supported by |
| * browsers that support HTTP Access Control. As of this writing, this option |
| * is not supported in IE. |
| * |
| * @param {boolean} withCredentials Whether this should be a "credentialed" |
| * request. |
| */ |
| goog.net.XhrIo.prototype.setWithCredentials = function(withCredentials) { |
| this.withCredentials_ = withCredentials; |
| }; |
| |
| |
| /** |
| * Gets whether a "credentialed" request is to be sent. |
| * @return {boolean} The desired type for the response. |
| */ |
| goog.net.XhrIo.prototype.getWithCredentials = function() { |
| return this.withCredentials_; |
| }; |
| |
| |
| /** |
| * Instance send that actually uses XMLHttpRequest to make a server call. |
| * @param {string|goog.Uri} url Uri to make request to. |
| * @param {string=} opt_method Send method, default: GET. |
| * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=} |
| * opt_content Body data. |
| * @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the |
| * request. |
| */ |
| goog.net.XhrIo.prototype.send = function(url, opt_method, opt_content, |
| opt_headers) { |
| if (this.xhr_) { |
| throw Error('[goog.net.XhrIo] Object is active with another request=' + |
| this.lastUri_ + '; newUri=' + url); |
| } |
| |
| var method = opt_method ? opt_method.toUpperCase() : 'GET'; |
| |
| this.lastUri_ = url; |
| this.lastError_ = ''; |
| this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR; |
| this.lastMethod_ = method; |
| this.errorDispatched_ = false; |
| this.active_ = true; |
| |
| // Use the factory to create the XHR object and options |
| this.xhr_ = this.createXhr(); |
| this.xhrOptions_ = this.xmlHttpFactory_ ? |
| this.xmlHttpFactory_.getOptions() : goog.net.XmlHttp.getOptions(); |
| |
| // Set up the onreadystatechange callback |
| this.xhr_.onreadystatechange = goog.bind(this.onReadyStateChange_, this); |
| |
| /** |
| * Try to open the XMLHttpRequest (always async), if an error occurs here it |
| * is generally permission denied |
| * @preserveTry |
| */ |
| try { |
| goog.log.fine(this.logger_, this.formatMsg_('Opening Xhr')); |
| this.inOpen_ = true; |
| this.xhr_.open(method, String(url), true); // Always async! |
| this.inOpen_ = false; |
| } catch (err) { |
| goog.log.fine(this.logger_, |
| this.formatMsg_('Error opening Xhr: ' + err.message)); |
| this.error_(goog.net.ErrorCode.EXCEPTION, err); |
| return; |
| } |
| |
| // We can't use null since this won't allow requests with form data to have a |
| // content length specified which will cause some proxies to return a 411 |
| // error. |
| var content = opt_content || ''; |
| |
| var headers = this.headers.clone(); |
| |
| // Add headers specific to this request |
| if (opt_headers) { |
| goog.structs.forEach(opt_headers, function(value, key) { |
| headers.set(key, value); |
| }); |
| } |
| |
| // Find whether a content type header is set, ignoring case. |
| // HTTP header names are case-insensitive. See: |
| // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 |
| var contentTypeKey = goog.array.find(headers.getKeys(), |
| goog.net.XhrIo.isContentTypeHeader_); |
| |
| var contentIsFormData = (goog.global['FormData'] && |
| (content instanceof goog.global['FormData'])); |
| if (goog.array.contains(goog.net.XhrIo.METHODS_WITH_FORM_DATA, method) && |
| !contentTypeKey && !contentIsFormData) { |
| // For requests typically with form data, default to the url-encoded form |
| // content type unless this is a FormData request. For FormData, |
| // the browser will automatically add a multipart/form-data content type |
| // with an appropriate multipart boundary. |
| headers.set(goog.net.XhrIo.CONTENT_TYPE_HEADER, |
| goog.net.XhrIo.FORM_CONTENT_TYPE); |
| } |
| |
| // Add the headers to the Xhr object |
| headers.forEach(function(value, key) { |
| this.xhr_.setRequestHeader(key, value); |
| }, this); |
| |
| if (this.responseType_) { |
| this.xhr_.responseType = this.responseType_; |
| } |
| |
| if (goog.object.containsKey(this.xhr_, 'withCredentials')) { |
| this.xhr_.withCredentials = this.withCredentials_; |
| } |
| |
| /** |
| * Try to send the request, or other wise report an error (404 not found). |
| * @preserveTry |
| */ |
| try { |
| this.cleanUpTimeoutTimer_(); // Paranoid, should never be running. |
| if (this.timeoutInterval_ > 0) { |
| this.useXhr2Timeout_ = goog.net.XhrIo.shouldUseXhr2Timeout_(this.xhr_); |
| goog.log.fine(this.logger_, this.formatMsg_('Will abort after ' + |
| this.timeoutInterval_ + 'ms if incomplete, xhr2 ' + |
| this.useXhr2Timeout_)); |
| if (this.useXhr2Timeout_) { |
| this.xhr_[goog.net.XhrIo.XHR2_TIMEOUT_] = this.timeoutInterval_; |
| this.xhr_[goog.net.XhrIo.XHR2_ON_TIMEOUT_] = |
| goog.bind(this.timeout_, this); |
| } else { |
| this.timeoutId_ = goog.Timer.callOnce(this.timeout_, |
| this.timeoutInterval_, this); |
| } |
| } |
| goog.log.fine(this.logger_, this.formatMsg_('Sending request')); |
| this.inSend_ = true; |
| this.xhr_.send(content); |
| this.inSend_ = false; |
| |
| } catch (err) { |
| goog.log.fine(this.logger_, this.formatMsg_('Send error: ' + err.message)); |
| this.error_(goog.net.ErrorCode.EXCEPTION, err); |
| } |
| }; |
| |
| |
| /** |
| * Determines if the argument is an XMLHttpRequest that supports the level 2 |
| * timeout value and event. |
| * |
| * Currently, FF 21.0 OS X has the fields but won't actually call the timeout |
| * handler. Perhaps the confusion in the bug referenced below hasn't |
| * entirely been resolved. |
| * |
| * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute |
| * @see https://bugzilla.mozilla.org/show_bug.cgi?id=525816 |
| * |
| * @param {!goog.net.XhrLike.OrNative} xhr The request. |
| * @return {boolean} True if the request supports level 2 timeout. |
| * @private |
| */ |
| goog.net.XhrIo.shouldUseXhr2Timeout_ = function(xhr) { |
| return goog.userAgent.IE && |
| goog.userAgent.isVersionOrHigher(9) && |
| goog.isNumber(xhr[goog.net.XhrIo.XHR2_TIMEOUT_]) && |
| goog.isDef(xhr[goog.net.XhrIo.XHR2_ON_TIMEOUT_]); |
| }; |
| |
| |
| /** |
| * @param {string} header An HTTP header key. |
| * @return {boolean} Whether the key is a content type header (ignoring |
| * case. |
| * @private |
| */ |
| goog.net.XhrIo.isContentTypeHeader_ = function(header) { |
| return goog.string.caseInsensitiveEquals( |
| goog.net.XhrIo.CONTENT_TYPE_HEADER, header); |
| }; |
| |
| |
| /** |
| * Creates a new XHR object. |
| * @return {!goog.net.XhrLike.OrNative} The newly created XHR object. |
| * @protected |
| */ |
| goog.net.XhrIo.prototype.createXhr = function() { |
| return this.xmlHttpFactory_ ? |
| this.xmlHttpFactory_.createInstance() : goog.net.XmlHttp(); |
| }; |
| |
| |
| /** |
| * The request didn't complete after {@link goog.net.XhrIo#timeoutInterval_} |
| * milliseconds; raises a {@link goog.net.EventType.TIMEOUT} event and aborts |
| * the request. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.timeout_ = function() { |
| if (typeof goog == 'undefined') { |
| // If goog is undefined then the callback has occurred as the application |
| // is unloading and will error. Thus we let it silently fail. |
| } else if (this.xhr_) { |
| this.lastError_ = 'Timed out after ' + this.timeoutInterval_ + |
| 'ms, aborting'; |
| this.lastErrorCode_ = goog.net.ErrorCode.TIMEOUT; |
| goog.log.fine(this.logger_, this.formatMsg_(this.lastError_)); |
| this.dispatchEvent(goog.net.EventType.TIMEOUT); |
| this.abort(goog.net.ErrorCode.TIMEOUT); |
| } |
| }; |
| |
| |
| /** |
| * Something errorred, so inactivate, fire error callback and clean up |
| * @param {goog.net.ErrorCode} errorCode The error code. |
| * @param {Error} err The error object. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.error_ = function(errorCode, err) { |
| this.active_ = false; |
| if (this.xhr_) { |
| this.inAbort_ = true; |
| this.xhr_.abort(); // Ensures XHR isn't hung (FF) |
| this.inAbort_ = false; |
| } |
| this.lastError_ = err; |
| this.lastErrorCode_ = errorCode; |
| this.dispatchErrors_(); |
| this.cleanUpXhr_(); |
| }; |
| |
| |
| /** |
| * Dispatches COMPLETE and ERROR in case of an error. This ensures that we do |
| * not dispatch multiple error events. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.dispatchErrors_ = function() { |
| if (!this.errorDispatched_) { |
| this.errorDispatched_ = true; |
| this.dispatchEvent(goog.net.EventType.COMPLETE); |
| this.dispatchEvent(goog.net.EventType.ERROR); |
| } |
| }; |
| |
| |
| /** |
| * Abort the current XMLHttpRequest |
| * @param {goog.net.ErrorCode=} opt_failureCode Optional error code to use - |
| * defaults to ABORT. |
| */ |
| goog.net.XhrIo.prototype.abort = function(opt_failureCode) { |
| if (this.xhr_ && this.active_) { |
| goog.log.fine(this.logger_, this.formatMsg_('Aborting')); |
| this.active_ = false; |
| this.inAbort_ = true; |
| this.xhr_.abort(); |
| this.inAbort_ = false; |
| this.lastErrorCode_ = opt_failureCode || goog.net.ErrorCode.ABORT; |
| this.dispatchEvent(goog.net.EventType.COMPLETE); |
| this.dispatchEvent(goog.net.EventType.ABORT); |
| this.cleanUpXhr_(); |
| } |
| }; |
| |
| |
| /** |
| * Nullifies all callbacks to reduce risks of leaks. |
| * @override |
| * @protected |
| */ |
| goog.net.XhrIo.prototype.disposeInternal = function() { |
| if (this.xhr_) { |
| // We explicitly do not call xhr_.abort() unless active_ is still true. |
| // This is to avoid unnecessarily aborting a successful request when |
| // dispose() is called in a callback triggered by a complete response, but |
| // in which browser cleanup has not yet finished. |
| // (See http://b/issue?id=1684217.) |
| if (this.active_) { |
| this.active_ = false; |
| this.inAbort_ = true; |
| this.xhr_.abort(); |
| this.inAbort_ = false; |
| } |
| this.cleanUpXhr_(true); |
| } |
| |
| goog.net.XhrIo.base(this, 'disposeInternal'); |
| }; |
| |
| |
| /** |
| * Internal handler for the XHR object's readystatechange event. This method |
| * checks the status and the readystate and fires the correct callbacks. |
| * If the request has ended, the handlers are cleaned up and the XHR object is |
| * nullified. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.onReadyStateChange_ = function() { |
| if (this.isDisposed()) { |
| // This method is the target of an untracked goog.Timer.callOnce(). |
| return; |
| } |
| if (!this.inOpen_ && !this.inSend_ && !this.inAbort_) { |
| // Were not being called from within a call to this.xhr_.send |
| // this.xhr_.abort, or this.xhr_.open, so this is an entry point |
| this.onReadyStateChangeEntryPoint_(); |
| } else { |
| this.onReadyStateChangeHelper_(); |
| } |
| }; |
| |
| |
| /** |
| * Used to protect the onreadystatechange handler entry point. Necessary |
| * as {#onReadyStateChange_} maybe called from within send or abort, this |
| * method is only called when {#onReadyStateChange_} is called as an |
| * entry point. |
| * {@see #protectEntryPoints} |
| * @private |
| */ |
| goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = function() { |
| this.onReadyStateChangeHelper_(); |
| }; |
| |
| |
| /** |
| * Helper for {@link #onReadyStateChange_}. This is used so that |
| * entry point calls to {@link #onReadyStateChange_} can be routed through |
| * {@link #onReadyStateChangeEntryPoint_}. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.onReadyStateChangeHelper_ = function() { |
| if (!this.active_) { |
| // can get called inside abort call |
| return; |
| } |
| |
| if (typeof goog == 'undefined') { |
| // NOTE(user): If goog is undefined then the callback has occurred as the |
| // application is unloading and will error. Thus we let it silently fail. |
| |
| } else if ( |
| this.xhrOptions_[goog.net.XmlHttp.OptionType.LOCAL_REQUEST_ERROR] && |
| this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE && |
| this.getStatus() == 2) { |
| // NOTE(user): In IE if send() errors on a *local* request the readystate |
| // is still changed to COMPLETE. We need to ignore it and allow the |
| // try/catch around send() to pick up the error. |
| goog.log.fine(this.logger_, this.formatMsg_( |
| 'Local request error detected and ignored')); |
| |
| } else { |
| |
| // In IE when the response has been cached we sometimes get the callback |
| // from inside the send call and this usually breaks code that assumes that |
| // XhrIo is asynchronous. If that is the case we delay the callback |
| // using a timer. |
| if (this.inSend_ && |
| this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE) { |
| goog.Timer.callOnce(this.onReadyStateChange_, 0, this); |
| return; |
| } |
| |
| this.dispatchEvent(goog.net.EventType.READY_STATE_CHANGE); |
| |
| // readyState indicates the transfer has finished |
| if (this.isComplete()) { |
| goog.log.fine(this.logger_, this.formatMsg_('Request complete')); |
| |
| this.active_ = false; |
| |
| try { |
| // Call the specific callbacks for success or failure. Only call the |
| // success if the status is 200 (HTTP_OK) or 304 (HTTP_CACHED) |
| if (this.isSuccess()) { |
| this.dispatchEvent(goog.net.EventType.COMPLETE); |
| this.dispatchEvent(goog.net.EventType.SUCCESS); |
| } else { |
| this.lastErrorCode_ = goog.net.ErrorCode.HTTP_ERROR; |
| this.lastError_ = |
| this.getStatusText() + ' [' + this.getStatus() + ']'; |
| this.dispatchErrors_(); |
| } |
| } finally { |
| this.cleanUpXhr_(); |
| } |
| } |
| } |
| }; |
| |
| |
| /** |
| * Remove the listener to protect against leaks, and nullify the XMLHttpRequest |
| * object. |
| * @param {boolean=} opt_fromDispose If this is from the dispose (don't want to |
| * fire any events). |
| * @private |
| */ |
| goog.net.XhrIo.prototype.cleanUpXhr_ = function(opt_fromDispose) { |
| if (this.xhr_) { |
| // Cancel any pending timeout event handler. |
| this.cleanUpTimeoutTimer_(); |
| |
| // Save reference so we can mark it as closed after the READY event. The |
| // READY event may trigger another request, thus we must nullify this.xhr_ |
| var xhr = this.xhr_; |
| var clearedOnReadyStateChange = |
| this.xhrOptions_[goog.net.XmlHttp.OptionType.USE_NULL_FUNCTION] ? |
| goog.nullFunction : null; |
| this.xhr_ = null; |
| this.xhrOptions_ = null; |
| |
| if (!opt_fromDispose) { |
| this.dispatchEvent(goog.net.EventType.READY); |
| } |
| |
| try { |
| // NOTE(user): Not nullifying in FireFox can still leak if the callbacks |
| // are defined in the same scope as the instance of XhrIo. But, IE doesn't |
| // allow you to set the onreadystatechange to NULL so nullFunction is |
| // used. |
| xhr.onreadystatechange = clearedOnReadyStateChange; |
| } catch (e) { |
| // This seems to occur with a Gears HTTP request. Delayed the setting of |
| // this onreadystatechange until after READY is sent out and catching the |
| // error to see if we can track down the problem. |
| goog.log.error(this.logger_, |
| 'Problem encountered resetting onreadystatechange: ' + e.message); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Make sure the timeout timer isn't running. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.cleanUpTimeoutTimer_ = function() { |
| if (this.xhr_ && this.useXhr2Timeout_) { |
| this.xhr_[goog.net.XhrIo.XHR2_ON_TIMEOUT_] = null; |
| } |
| if (goog.isNumber(this.timeoutId_)) { |
| goog.Timer.clear(this.timeoutId_); |
| this.timeoutId_ = null; |
| } |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether there is an active request. |
| */ |
| goog.net.XhrIo.prototype.isActive = function() { |
| return !!this.xhr_; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the request has completed. |
| */ |
| goog.net.XhrIo.prototype.isComplete = function() { |
| return this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE; |
| }; |
| |
| |
| /** |
| * @return {boolean} Whether the request completed with a success. |
| */ |
| goog.net.XhrIo.prototype.isSuccess = function() { |
| var status = this.getStatus(); |
| // A zero status code is considered successful for local files. |
| return goog.net.HttpStatus.isSuccess(status) || |
| status === 0 && !this.isLastUriEffectiveSchemeHttp_(); |
| }; |
| |
| |
| /** |
| * @return {boolean} whether the effective scheme of the last URI that was |
| * fetched was 'http' or 'https'. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.isLastUriEffectiveSchemeHttp_ = function() { |
| var scheme = goog.uri.utils.getEffectiveScheme(String(this.lastUri_)); |
| return goog.net.XhrIo.HTTP_SCHEME_PATTERN.test(scheme); |
| }; |
| |
| |
| /** |
| * Get the readystate from the Xhr object |
| * Will only return correct result when called from the context of a callback |
| * @return {goog.net.XmlHttp.ReadyState} goog.net.XmlHttp.ReadyState.*. |
| */ |
| goog.net.XhrIo.prototype.getReadyState = function() { |
| return this.xhr_ ? |
| /** @type {goog.net.XmlHttp.ReadyState} */ (this.xhr_.readyState) : |
| goog.net.XmlHttp.ReadyState.UNINITIALIZED; |
| }; |
| |
| |
| /** |
| * Get the status from the Xhr object |
| * Will only return correct result when called from the context of a callback |
| * @return {number} Http status. |
| */ |
| goog.net.XhrIo.prototype.getStatus = function() { |
| /** |
| * IE doesn't like you checking status until the readystate is greater than 2 |
| * (i.e. it is receiving or complete). The try/catch is used for when the |
| * page is unloading and an ERROR_NOT_AVAILABLE may occur when accessing xhr_. |
| * @preserveTry |
| */ |
| try { |
| return this.getReadyState() > goog.net.XmlHttp.ReadyState.LOADED ? |
| this.xhr_.status : -1; |
| } catch (e) { |
| return -1; |
| } |
| }; |
| |
| |
| /** |
| * Get the status text from the Xhr object |
| * Will only return correct result when called from the context of a callback |
| * @return {string} Status text. |
| */ |
| goog.net.XhrIo.prototype.getStatusText = function() { |
| /** |
| * IE doesn't like you checking status until the readystate is greater than 2 |
| * (i.e. it is recieving or complete). The try/catch is used for when the |
| * page is unloading and an ERROR_NOT_AVAILABLE may occur when accessing xhr_. |
| * @preserveTry |
| */ |
| try { |
| return this.getReadyState() > goog.net.XmlHttp.ReadyState.LOADED ? |
| this.xhr_.statusText : ''; |
| } catch (e) { |
| goog.log.fine(this.logger_, 'Can not get status: ' + e.message); |
| return ''; |
| } |
| }; |
| |
| |
| /** |
| * Get the last Uri that was requested |
| * @return {string} Last Uri. |
| */ |
| goog.net.XhrIo.prototype.getLastUri = function() { |
| return String(this.lastUri_); |
| }; |
| |
| |
| /** |
| * Get the response text from the Xhr object |
| * Will only return correct result when called from the context of a callback. |
| * @return {string} Result from the server, or '' if no result available. |
| */ |
| goog.net.XhrIo.prototype.getResponseText = function() { |
| /** @preserveTry */ |
| try { |
| return this.xhr_ ? this.xhr_.responseText : ''; |
| } catch (e) { |
| // http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute |
| // states that responseText should return '' (and responseXML null) |
| // when the state is not LOADING or DONE. Instead, IE can |
| // throw unexpected exceptions, for example when a request is aborted |
| // or no data is available yet. |
| goog.log.fine(this.logger_, 'Can not get responseText: ' + e.message); |
| return ''; |
| } |
| }; |
| |
| |
| /** |
| * Get the response body from the Xhr object. This property is only available |
| * in IE since version 7 according to MSDN: |
| * http://msdn.microsoft.com/en-us/library/ie/ms534368(v=vs.85).aspx |
| * Will only return correct result when called from the context of a callback. |
| * |
| * One option is to construct a VBArray from the returned object and convert |
| * it to a JavaScript array using the toArray method: |
| * {@code (new window['VBArray'](xhrIo.getResponseBody())).toArray()} |
| * This will result in an array of numbers in the range of [0..255] |
| * |
| * Another option is to use the VBScript CStr method to convert it into a |
| * string as outlined in http://stackoverflow.com/questions/1919972 |
| * |
| * @return {Object} Binary result from the server or null if not available. |
| */ |
| goog.net.XhrIo.prototype.getResponseBody = function() { |
| /** @preserveTry */ |
| try { |
| if (this.xhr_ && 'responseBody' in this.xhr_) { |
| return this.xhr_['responseBody']; |
| } |
| } catch (e) { |
| // IE can throw unexpected exceptions, for example when a request is aborted |
| // or no data is yet available. |
| goog.log.fine(this.logger_, 'Can not get responseBody: ' + e.message); |
| } |
| return null; |
| }; |
| |
| |
| /** |
| * Get the response XML from the Xhr object |
| * Will only return correct result when called from the context of a callback. |
| * @return {Document} The DOM Document representing the XML file, or null |
| * if no result available. |
| */ |
| goog.net.XhrIo.prototype.getResponseXml = function() { |
| /** @preserveTry */ |
| try { |
| return this.xhr_ ? this.xhr_.responseXML : null; |
| } catch (e) { |
| goog.log.fine(this.logger_, 'Can not get responseXML: ' + e.message); |
| return null; |
| } |
| }; |
| |
| |
| /** |
| * Get the response and evaluates it as JSON from the Xhr object |
| * Will only return correct result when called from the context of a callback |
| * @param {string=} opt_xssiPrefix Optional XSSI prefix string to use for |
| * stripping of the response before parsing. This needs to be set only if |
| * your backend server prepends the same prefix string to the JSON response. |
| * @return {Object|undefined} JavaScript object. |
| */ |
| goog.net.XhrIo.prototype.getResponseJson = function(opt_xssiPrefix) { |
| if (!this.xhr_) { |
| return undefined; |
| } |
| |
| var responseText = this.xhr_.responseText; |
| if (opt_xssiPrefix && responseText.indexOf(opt_xssiPrefix) == 0) { |
| responseText = responseText.substring(opt_xssiPrefix.length); |
| } |
| |
| return goog.json.parse(responseText); |
| }; |
| |
| |
| /** |
| * Get the response as the type specificed by {@link #setResponseType}. At time |
| * of writing, this is only directly supported in very recent versions of WebKit |
| * (10.0.612.1 dev and later). If the field is not supported directly, we will |
| * try to emulate it. |
| * |
| * Emulating the response means following the rules laid out at |
| * http://www.w3.org/TR/XMLHttpRequest/#the-response-attribute |
| * |
| * On browsers with no support for this (Chrome < 10, Firefox < 4, etc), only |
| * response types of DEFAULT or TEXT may be used, and the response returned will |
| * be the text response. |
| * |
| * On browsers with Mozilla's draft support for array buffers (Firefox 4, 5), |
| * only response types of DEFAULT, TEXT, and ARRAY_BUFFER may be used, and the |
| * response returned will be either the text response or the Mozilla |
| * implementation of the array buffer response. |
| * |
| * On browsers will full support, any valid response type supported by the |
| * browser may be used, and the response provided by the browser will be |
| * returned. |
| * |
| * @return {*} The response. |
| */ |
| goog.net.XhrIo.prototype.getResponse = function() { |
| /** @preserveTry */ |
| try { |
| if (!this.xhr_) { |
| return null; |
| } |
| if ('response' in this.xhr_) { |
| return this.xhr_.response; |
| } |
| switch (this.responseType_) { |
| case goog.net.XhrIo.ResponseType.DEFAULT: |
| case goog.net.XhrIo.ResponseType.TEXT: |
| return this.xhr_.responseText; |
| // DOCUMENT and BLOB don't need to be handled here because they are |
| // introduced in the same spec that adds the .response field, and would |
| // have been caught above. |
| // ARRAY_BUFFER needs an implementation for Firefox 4, where it was |
| // implemented using a draft spec rather than the final spec. |
| case goog.net.XhrIo.ResponseType.ARRAY_BUFFER: |
| if ('mozResponseArrayBuffer' in this.xhr_) { |
| return this.xhr_.mozResponseArrayBuffer; |
| } |
| } |
| // Fell through to a response type that is not supported on this browser. |
| goog.log.error(this.logger_, |
| 'Response type ' + this.responseType_ + ' is not ' + |
| 'supported on this browser'); |
| return null; |
| } catch (e) { |
| goog.log.fine(this.logger_, 'Can not get response: ' + e.message); |
| return null; |
| } |
| }; |
| |
| |
| /** |
| * Get the value of the response-header with the given name from the Xhr object |
| * Will only return correct result when called from the context of a callback |
| * and the request has completed |
| * @param {string} key The name of the response-header to retrieve. |
| * @return {string|undefined} The value of the response-header named key. |
| */ |
| goog.net.XhrIo.prototype.getResponseHeader = function(key) { |
| return this.xhr_ && this.isComplete() ? |
| this.xhr_.getResponseHeader(key) : undefined; |
| }; |
| |
| |
| /** |
| * Gets the text of all the headers in the response. |
| * Will only return correct result when called from the context of a callback |
| * and the request has completed. |
| * @return {string} The value of the response headers or empty string. |
| */ |
| goog.net.XhrIo.prototype.getAllResponseHeaders = function() { |
| return this.xhr_ && this.isComplete() ? |
| this.xhr_.getAllResponseHeaders() : ''; |
| }; |
| |
| |
| /** |
| * Returns all response headers as a key-value map. |
| * Multiple values for the same header key can be combined into one, |
| * separated by a comma and a space. |
| * Note that the native getResponseHeader method for retrieving a single header |
| * does a case insensitive match on the header name. This method does not |
| * include any case normalization logic, it will just return a key-value |
| * representation of the headers. |
| * See: http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method |
| * @return {!Object<string, string>} An object with the header keys as keys |
| * and header values as values. |
| */ |
| goog.net.XhrIo.prototype.getResponseHeaders = function() { |
| var headersObject = {}; |
| var headersArray = this.getAllResponseHeaders().split('\r\n'); |
| for (var i = 0; i < headersArray.length; i++) { |
| if (goog.string.isEmptyOrWhitespace(headersArray[i])) { |
| continue; |
| } |
| var keyValue = goog.string.splitLimit(headersArray[i], ': ', 2); |
| if (headersObject[keyValue[0]]) { |
| headersObject[keyValue[0]] += ', ' + keyValue[1]; |
| } else { |
| headersObject[keyValue[0]] = keyValue[1]; |
| } |
| } |
| return headersObject; |
| }; |
| |
| |
| /** |
| * Get the last error message |
| * @return {goog.net.ErrorCode} Last error code. |
| */ |
| goog.net.XhrIo.prototype.getLastErrorCode = function() { |
| return this.lastErrorCode_; |
| }; |
| |
| |
| /** |
| * Get the last error message |
| * @return {string} Last error message. |
| */ |
| goog.net.XhrIo.prototype.getLastError = function() { |
| return goog.isString(this.lastError_) ? this.lastError_ : |
| String(this.lastError_); |
| }; |
| |
| |
| /** |
| * Adds the last method, status and URI to the message. This is used to add |
| * this information to the logging calls. |
| * @param {string} msg The message text that we want to add the extra text to. |
| * @return {string} The message with the extra text appended. |
| * @private |
| */ |
| goog.net.XhrIo.prototype.formatMsg_ = function(msg) { |
| return msg + ' [' + this.lastMethod_ + ' ' + this.lastUri_ + ' ' + |
| this.getStatus() + ']'; |
| }; |
| |
| |
| // Register the xhr handler as an entry point, so that |
| // it can be monitored for exception handling, etc. |
| goog.debug.entryPointRegistry.register( |
| /** |
| * @param {function(!Function): !Function} transformer The transforming |
| * function. |
| */ |
| function(transformer) { |
| goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = |
| transformer(goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_); |
| }); |