450 lines
14 KiB
JavaScript
450 lines
14 KiB
JavaScript
// Copyright 2011 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 Offered as an alternative to XhrIo as a way for making requests
|
|
* via XMLHttpRequest. Instead of mirroring the XHR interface and exposing
|
|
* events, results are used as a way to pass a "promise" of the response to
|
|
* interested parties.
|
|
*
|
|
*/
|
|
|
|
goog.provide('goog.labs.net.xhr');
|
|
goog.provide('goog.labs.net.xhr.Error');
|
|
goog.provide('goog.labs.net.xhr.HttpError');
|
|
goog.provide('goog.labs.net.xhr.TimeoutError');
|
|
|
|
goog.require('goog.debug.Error');
|
|
goog.require('goog.json');
|
|
goog.require('goog.net.HttpStatus');
|
|
goog.require('goog.net.XmlHttp');
|
|
goog.require('goog.result');
|
|
goog.require('goog.result.SimpleResult');
|
|
goog.require('goog.string');
|
|
goog.require('goog.uri.utils');
|
|
|
|
|
|
|
|
goog.scope(function() {
|
|
var _ = goog.labs.net.xhr;
|
|
var Result = goog.result.Result;
|
|
var SimpleResult = goog.result.SimpleResult;
|
|
var Wait = goog.result.wait;
|
|
var HttpStatus = goog.net.HttpStatus;
|
|
|
|
|
|
/**
|
|
* Configuration options for an XMLHttpRequest.
|
|
* - headers: map of header key/value pairs.
|
|
* - timeoutMs: number of milliseconds after which the request will be timed
|
|
* out by the client. Default is to allow the browser to handle timeouts.
|
|
* - withCredentials: whether user credentials are to be included in a
|
|
* cross-origin request. See:
|
|
* http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
|
|
* - mimeType: allows the caller to override the content-type and charset for
|
|
* the request, which is useful when requesting binary data. See:
|
|
* http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
|
|
* - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
|
|
* be removed before parsing the response as JSON.
|
|
*
|
|
* @typedef {{
|
|
* headers: (Object.<string>|undefined),
|
|
* timeoutMs: (number|undefined),
|
|
* withCredentials: (boolean|undefined),
|
|
* mimeType: (string|undefined),
|
|
* xssiPrefix: (string|undefined)
|
|
* }}
|
|
*/
|
|
_.Options;
|
|
|
|
|
|
/**
|
|
* Defines the types that are allowed as post data.
|
|
* @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
|
|
*/
|
|
_.PostData;
|
|
|
|
|
|
/**
|
|
* The Content-Type HTTP header name.
|
|
* @type {string}
|
|
*/
|
|
_.CONTENT_TYPE_HEADER = 'Content-Type';
|
|
|
|
|
|
/**
|
|
* The Content-Type HTTP header value for a url-encoded form.
|
|
* @type {string}
|
|
*/
|
|
_.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
|
|
|
|
|
|
/**
|
|
* Sends a get request, returning a transformed result which will be resolved
|
|
* with the response text once the request completes.
|
|
*
|
|
* @param {string} url The URL to request.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @return {!Result} A result object that will be resolved
|
|
* with the response text once the request finishes.
|
|
*/
|
|
_.get = function(url, opt_options) {
|
|
var result = _.send('GET', url, null, opt_options);
|
|
var transformedResult = goog.result.transform(result,
|
|
_.getResponseText_);
|
|
return transformedResult;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sends a post request, returning a transformed result which will be resolved
|
|
* with the response text once the request completes.
|
|
*
|
|
* @param {string} url The URL to request.
|
|
* @param {_.PostData} data The body of the post request.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @return {!Result} A result object that will be resolved
|
|
* with the response text once the request finishes.
|
|
*/
|
|
_.post = function(url, data, opt_options) {
|
|
var result = _.send('POST', url, data, opt_options);
|
|
var transformedResult = goog.result.transform(result,
|
|
_.getResponseText_);
|
|
return transformedResult;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sends a get request, returning a result which will be resolved with
|
|
* the parsed response text once the request completes.
|
|
*
|
|
* @param {string} url The URL to request.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @return {!Result} A result object that will be resolved
|
|
* with the response JSON once the request finishes.
|
|
*/
|
|
_.getJson = function(url, opt_options) {
|
|
var result = _.send('GET', url, null, opt_options);
|
|
var transformedResult = _.addJsonParsingCallbacks_(result, opt_options);
|
|
return transformedResult;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sends a post request, returning a result which will be resolved with
|
|
* the parsed response text once the request completes.
|
|
*
|
|
* @param {string} url The URL to request.
|
|
* @param {_.PostData} data The body of the post request.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @return {!Result} A result object that will be resolved
|
|
* with the response JSON once the request finishes.
|
|
*/
|
|
_.postJson = function(url, data, opt_options) {
|
|
var result = _.send('POST', url, data, opt_options);
|
|
var transformedResult = _.addJsonParsingCallbacks_(result, opt_options);
|
|
return transformedResult;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sends a request using XMLHttpRequest and returns a result.
|
|
*
|
|
* @param {string} method The HTTP method for the request.
|
|
* @param {string} url The URL to request.
|
|
* @param {_.PostData} data The body of the post request.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @return {!Result} A result object that will be resolved
|
|
* with the XHR object as it's value when the request finishes.
|
|
*/
|
|
_.send = function(method, url, data, opt_options) {
|
|
|
|
var result = new SimpleResult();
|
|
|
|
// When the deferred is cancelled, we abort the XHR. We want to make sure
|
|
// the readystatechange event still fires, so it can do the timeout
|
|
// cleanup, however we don't want the callback or errback to be called
|
|
// again. Thus the slight ugliness here. If results were pushed into
|
|
// makeRequest, this could become a lot cleaner but we want an option for
|
|
// people not to include goog.result.Result.
|
|
goog.result.waitOnError(result, function(error, result) {
|
|
if (result.isCanceled()) {
|
|
xhr.abort();
|
|
xhr.onreadystatechange = goog.nullFunction;
|
|
}
|
|
});
|
|
|
|
function callback(data) {
|
|
result.setValue(data);
|
|
}
|
|
|
|
function errback(err) {
|
|
result.setError(err);
|
|
}
|
|
|
|
var xhr = _.makeRequest(method, url, data, opt_options, callback, errback);
|
|
|
|
return result;
|
|
};
|
|
|
|
|
|
/**
|
|
* Creates a new XMLHttpRequest and initiates a request.
|
|
*
|
|
* @param {string} method The HTTP method for the request.
|
|
* @param {string} url The URL to request.
|
|
* @param {_.PostData} data The body of the post request, unless the content
|
|
* type is explicitly set in the Options, then it will default to form
|
|
* urlencoded.
|
|
* @param {_.Options=} opt_options Configuration options for the request.
|
|
* @param {function(XMLHttpRequest)=} opt_callback Optional callback to call
|
|
* when the request completes.
|
|
* @param {function(Error)=} opt_errback Optional callback to call
|
|
* when there is an error.
|
|
* @return {!XMLHttpRequest} The new XMLHttpRequest.
|
|
*/
|
|
_.makeRequest = function(
|
|
method, url, data, opt_options, opt_callback, opt_errback) {
|
|
var options = opt_options || {};
|
|
var callback = opt_callback || goog.nullFunction;
|
|
var errback = opt_errback || goog.nullFunction;
|
|
var timer;
|
|
|
|
var xhr = /** @type {!XMLHttpRequest} */ (goog.net.XmlHttp());
|
|
try {
|
|
xhr.open(method, url, true);
|
|
} catch (e) {
|
|
// XMLHttpRequest.open may throw when 'open' is called, for example, IE7
|
|
// throws "Access Denied" for cross-origin requests.
|
|
errback(new _.Error('Error opening XHR: ' + e.message, url, xhr));
|
|
return xhr;
|
|
}
|
|
|
|
// So sad that IE doesn't support onload and onerror.
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
|
|
window.clearTimeout(timer);
|
|
// Note: When developing locally, XHRs to file:// schemes return a status
|
|
// code of 0. We mark that case as a success too.
|
|
if (HttpStatus.isSuccess(xhr.status) ||
|
|
xhr.status === 0 && !_.isEffectiveSchemeHttp_(url)) {
|
|
callback(xhr);
|
|
} else {
|
|
errback(new _.HttpError(xhr.status, url, xhr));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Set the headers.
|
|
var contentTypeIsSet = false;
|
|
if (options.headers) {
|
|
for (var key in options.headers) {
|
|
xhr.setRequestHeader(key, options.headers[key]);
|
|
}
|
|
contentTypeIsSet = _.CONTENT_TYPE_HEADER in options.headers;
|
|
}
|
|
|
|
// If a content type hasn't been set, default to form-urlencoded/UTF8 for
|
|
// POSTs. This is because some proxies have been known to reject posts
|
|
// without a content-type.
|
|
if (method == 'POST' && !contentTypeIsSet) {
|
|
xhr.setRequestHeader(_.CONTENT_TYPE_HEADER, _.FORM_CONTENT_TYPE);
|
|
}
|
|
|
|
// Set whether to pass cookies on cross-domain requests (if applicable).
|
|
// @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
|
|
if (options.withCredentials) {
|
|
xhr.withCredentials = options.withCredentials;
|
|
}
|
|
|
|
// Allow the request to override the mime type, useful for getting binary
|
|
// data from the server. e.g. 'text/plain; charset=x-user-defined'.
|
|
// @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
|
|
if (options.mimeType) {
|
|
xhr.overrideMimeType(options.mimeType);
|
|
}
|
|
|
|
// Handle timeouts, if requested.
|
|
if (options.timeoutMs > 0) {
|
|
timer = window.setTimeout(function() {
|
|
// Clear event listener before aborting so the errback will not be
|
|
// called twice.
|
|
xhr.onreadystatechange = goog.nullFunction;
|
|
xhr.abort();
|
|
errback(new _.TimeoutError(url, xhr));
|
|
}, options.timeoutMs);
|
|
}
|
|
|
|
// Trigger the send.
|
|
try {
|
|
xhr.send(data);
|
|
} catch (e) {
|
|
// XMLHttpRequest.send is known to throw on some versions of FF, for example
|
|
// if a cross-origin request is disallowed.
|
|
xhr.onreadystatechange = goog.nullFunction;
|
|
window.clearTimeout(timer);
|
|
errback(new _.Error('Error sending XHR: ' + e.message, url, xhr));
|
|
}
|
|
|
|
return xhr;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {string} url The URL to test.
|
|
* @return {boolean} Whether the effective scheme is HTTP or HTTPs.
|
|
* @private
|
|
*/
|
|
_.isEffectiveSchemeHttp_ = function(url) {
|
|
var scheme = goog.uri.utils.getEffectiveScheme(url);
|
|
// NOTE(user): Empty-string is for the case under FF3.5 when the location
|
|
// is not defined inside a web worker.
|
|
return scheme == 'http' || scheme == 'https' || scheme == '';
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the response text of an XHR object. Intended to be called when
|
|
* the result resolves.
|
|
*
|
|
* @param {!XMLHttpRequest} xhr The XHR object.
|
|
* @return {string} The response text.
|
|
* @private
|
|
*/
|
|
_.getResponseText_ = function(xhr) {
|
|
return xhr.responseText;
|
|
};
|
|
|
|
|
|
/**
|
|
* Transforms a result, parsing the JSON in the original result value's
|
|
* responseText. The transformed result's value is a javascript object.
|
|
* Parse errors resolve the transformed result in an error.
|
|
*
|
|
* @param {!Result} result The result to wait on.
|
|
* @param {_.Options|undefined} options The options object.
|
|
*
|
|
* @return {!Result} The transformed result.
|
|
* @private
|
|
*/
|
|
_.addJsonParsingCallbacks_ = function(result, options) {
|
|
var resultWithResponseText = goog.result.transform(result,
|
|
_.getResponseText_);
|
|
var prefixStrippedResult = resultWithResponseText;
|
|
if (options && options.xssiPrefix) {
|
|
prefixStrippedResult = goog.result.transform(resultWithResponseText,
|
|
goog.partial(_.stripXssiPrefix_, options.xssiPrefix));
|
|
}
|
|
|
|
var jsonParsedResult = goog.result.transform(prefixStrippedResult,
|
|
goog.json.parse);
|
|
return jsonParsedResult;
|
|
};
|
|
|
|
|
|
/**
|
|
* Strips the XSSI prefix from the input string.
|
|
*
|
|
* @param {string} prefix The XSSI prefix.
|
|
* @param {string} string The string to strip the prefix from.
|
|
* @return {string} The input string without the prefix.
|
|
* @private
|
|
*/
|
|
_.stripXssiPrefix_ = function(prefix, string) {
|
|
if (goog.string.startsWith(string, prefix)) {
|
|
string = string.substring(prefix.length);
|
|
}
|
|
return string;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Generic error that may occur during a request.
|
|
*
|
|
* @param {string} message The error message.
|
|
* @param {string} url The URL that was being requested.
|
|
* @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
|
|
* @extends {goog.debug.Error}
|
|
* @constructor
|
|
*/
|
|
_.Error = function(message, url, xhr) {
|
|
goog.base(this, message + ', url=' + url);
|
|
|
|
/**
|
|
* The URL that was requested.
|
|
* @type {string}
|
|
*/
|
|
this.url = url;
|
|
|
|
/**
|
|
* The XMLHttpRequest corresponding with the failed request.
|
|
* @type {!XMLHttpRequest}
|
|
*/
|
|
this.xhr = xhr;
|
|
};
|
|
goog.inherits(_.Error, goog.debug.Error);
|
|
|
|
|
|
/** @override */
|
|
_.Error.prototype.name = 'XhrError';
|
|
|
|
|
|
|
|
/**
|
|
* Class for HTTP errors.
|
|
*
|
|
* @param {number} status The HTTP status code of the response.
|
|
* @param {string} url The URL that was being requested.
|
|
* @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
|
|
* @extends {_.Error}
|
|
* @constructor
|
|
*/
|
|
_.HttpError = function(status, url, xhr) {
|
|
goog.base(this, 'Request Failed, status=' + status, url, xhr);
|
|
|
|
/**
|
|
* The HTTP status code for the error.
|
|
* @type {number}
|
|
*/
|
|
this.status = status;
|
|
};
|
|
goog.inherits(_.HttpError, _.Error);
|
|
|
|
|
|
/** @override */
|
|
_.HttpError.prototype.name = 'XhrHttpError';
|
|
|
|
|
|
|
|
/**
|
|
* Class for Timeout errors.
|
|
*
|
|
* @param {string} url The URL that timed out.
|
|
* @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
|
|
* @extends {_.Error}
|
|
* @constructor
|
|
*/
|
|
_.TimeoutError = function(url, xhr) {
|
|
goog.base(this, 'Request timed out', url, xhr);
|
|
};
|
|
goog.inherits(_.TimeoutError, _.Error);
|
|
|
|
|
|
/** @override */
|
|
_.TimeoutError.prototype.name = 'XhrTimeoutError';
|
|
|
|
}); // goog.scope
|