123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- // 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.Options');
- goog.provide('goog.labs.net.xhr.PostData');
- goog.provide('goog.labs.net.xhr.ResponseType');
- goog.provide('goog.labs.net.xhr.TimeoutError');
- goog.require('goog.Promise');
- goog.require('goog.asserts');
- goog.require('goog.debug.Error');
- goog.require('goog.json');
- goog.require('goog.net.HttpStatus');
- goog.require('goog.net.XmlHttp');
- goog.require('goog.object');
- goog.require('goog.string');
- goog.require('goog.uri.utils');
- goog.require('goog.userAgent');
- goog.scope(function() {
- var userAgent = goog.userAgent;
- var xhr = goog.labs.net.xhr;
- 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://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
- * - mimeType: allows the caller to override the content-type and charset for
- * the request. See:
- * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
- * - responseType: may be set to change the response type to an arraybuffer or
- * blob for downloading binary data. See:
- * http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype]
- * - xmlHttpFactory: allows the caller to override the factory used to create
- * XMLHttpRequest objects.
- * - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
- * be removed before parsing the response as JSON.
- *
- * @typedef {{
- * headers: (Object<string>|undefined),
- * mimeType: (string|undefined),
- * responseType: (xhr.ResponseType|undefined),
- * timeoutMs: (number|undefined),
- * withCredentials: (boolean|undefined),
- * xmlHttpFactory: (goog.net.XmlHttpFactory|undefined),
- * xssiPrefix: (string|undefined)
- * }}
- */
- xhr.Options;
- /**
- * Defines the types that are allowed as post data.
- * @typedef {(ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined)}
- */
- xhr.PostData;
- /**
- * The Content-Type HTTP header name.
- * @type {string}
- */
- xhr.CONTENT_TYPE_HEADER = 'Content-Type';
- /**
- * The Content-Type HTTP header value for a url-encoded form.
- * @type {string}
- */
- xhr.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';
- /**
- * Supported data types for the responseType field.
- * See: http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-response
- * @enum {string}
- */
- xhr.ResponseType = {
- ARRAYBUFFER: 'arraybuffer',
- BLOB: 'blob',
- DOCUMENT: 'document',
- JSON: 'json',
- TEXT: 'text'
- };
- /**
- * Sends a get request, returning a promise that will be resolved
- * with the response text once the request completes.
- *
- * @param {string} url The URL to request.
- * @param {xhr.Options=} opt_options Configuration options for the request.
- * @return {!goog.Promise<string>} A promise that will be resolved with the
- * response text once the request completes.
- */
- xhr.get = function(url, opt_options) {
- return xhr.send('GET', url, null, opt_options).then(function(request) {
- return request.responseText;
- });
- };
- /**
- * Sends a post request, returning a promise that will be resolved
- * with the response text once the request completes.
- *
- * @param {string} url The URL to request.
- * @param {xhr.PostData} data The body of the post request.
- * @param {xhr.Options=} opt_options Configuration options for the request.
- * @return {!goog.Promise<string>} A promise that will be resolved with the
- * response text once the request completes.
- */
- xhr.post = function(url, data, opt_options) {
- return xhr.send('POST', url, data, opt_options).then(function(request) {
- return request.responseText;
- });
- };
- /**
- * Sends a get request, returning a promise that will be resolved with
- * the parsed response text once the request completes.
- *
- * @param {string} url The URL to request.
- * @param {xhr.Options=} opt_options Configuration options for the request.
- * @return {!goog.Promise<Object>} A promise that will be resolved with the
- * response JSON once the request completes.
- */
- xhr.getJson = function(url, opt_options) {
- return xhr.send('GET', url, null, opt_options).then(function(request) {
- return xhr.parseJson_(request.responseText, opt_options);
- });
- };
- /**
- * Sends a get request, returning a promise that will be resolved with the
- * response as a Blob.
- *
- * @param {string} url The URL to request.
- * @param {xhr.Options=} opt_options Configuration options for the request. If
- * responseType is set, it will be ignored for this request.
- * @return {!goog.Promise<!Blob>} A promise that will be resolved with an
- * immutable Blob representing the file once the request completes.
- */
- xhr.getBlob = function(url, opt_options) {
- goog.asserts.assert(
- 'Blob' in goog.global, 'getBlob is not supported in this browser.');
- var options = opt_options ? goog.object.clone(opt_options) : {};
- options.responseType = xhr.ResponseType.BLOB;
- return xhr.send('GET', url, null, options).then(function(request) {
- return /** @type {!Blob} */ (request.response);
- });
- };
- /**
- * Sends a get request, returning a promise that will be resolved with the
- * response as an array of bytes.
- *
- * Supported in all XMLHttpRequest level 2 browsers, as well as IE9. IE8 and
- * earlier are not supported.
- *
- * @param {string} url The URL to request.
- * @param {xhr.Options=} opt_options Configuration options for the request. If
- * responseType is set, it will be ignored for this request.
- * @return {!goog.Promise<!Uint8Array|!Array<number>>} A promise that will be
- * resolved with an array of bytes once the request completes.
- */
- xhr.getBytes = function(url, opt_options) {
- goog.asserts.assert(
- !userAgent.IE || userAgent.isDocumentModeOrHigher(9),
- 'getBytes is not supported in this browser.');
- var options = opt_options ? goog.object.clone(opt_options) : {};
- options.responseType = xhr.ResponseType.ARRAYBUFFER;
- return xhr.send('GET', url, null, options).then(function(request) {
- // Use the ArrayBuffer response in browsers that support XMLHttpRequest2.
- // This covers nearly all modern browsers: http://caniuse.com/xhr2
- if (request.response) {
- return new Uint8Array(/** @type {!ArrayBuffer} */ (request.response));
- }
- // Fallback for IE9: the response may be accessed as an array of bytes with
- // the non-standard responseBody property, which can only be accessed as a
- // VBArray. IE7 and IE8 require significant amounts of VBScript to extract
- // the bytes.
- // See: http://stackoverflow.com/questions/1919972/
- if (goog.global['VBArray']) {
- return new goog.global['VBArray'](request['responseBody']).toArray();
- }
- // Nearly all common browsers are covered by the cases above. If downloading
- // binary files in older browsers is necessary, the MDN article "Sending and
- // Receiving Binary Data" provides techniques that may work with
- // XMLHttpRequest level 1 browsers: http://goo.gl/7lEuGN
- throw new xhr.Error(
- 'getBytes is not supported in this browser.', url, request);
- });
- };
- /**
- * Sends a post request, returning a promise that will be resolved with
- * the parsed response text once the request completes.
- *
- * @param {string} url The URL to request.
- * @param {xhr.PostData} data The body of the post request.
- * @param {xhr.Options=} opt_options Configuration options for the request.
- * @return {!goog.Promise<Object>} A promise that will be resolved with the
- * response JSON once the request completes.
- */
- xhr.postJson = function(url, data, opt_options) {
- return xhr.send('POST', url, data, opt_options).then(function(request) {
- return xhr.parseJson_(request.responseText, opt_options);
- });
- };
- /**
- * Sends a request, returning a promise that will be resolved
- * with the XHR object once the request completes.
- *
- * If content type hasn't been set in opt_options headers, and hasn't been
- * explicitly set to null, default to form-urlencoded/UTF8 for POSTs.
- *
- * @param {string} method The HTTP method for the request.
- * @param {string} url The URL to request.
- * @param {xhr.PostData} data The body of the post request.
- * @param {xhr.Options=} opt_options Configuration options for the request.
- * @return {!goog.Promise<!goog.net.XhrLike.OrNative>} A promise that will be
- * resolved with the XHR object once the request completes.
- */
- xhr.send = function(method, url, data, opt_options) {
- return new goog.Promise(function(resolve, reject) {
- var options = opt_options || {};
- var timer;
- var request = options.xmlHttpFactory ?
- options.xmlHttpFactory.createInstance() :
- goog.net.XmlHttp();
- try {
- request.open(method, url, true);
- } catch (e) {
- // XMLHttpRequest.open may throw when 'open' is called, for example, IE7
- // throws "Access Denied" for cross-origin requests.
- reject(new xhr.Error('Error opening XHR: ' + e.message, url, request));
- }
- // So sad that IE doesn't support onload and onerror.
- request.onreadystatechange = function() {
- if (request.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
- goog.global.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(request.status) ||
- request.status === 0 && !xhr.isEffectiveSchemeHttp_(url)) {
- resolve(request);
- } else {
- reject(new xhr.HttpError(request.status, url, request));
- }
- }
- };
- request.onerror = function() {
- reject(new xhr.Error('Network error', url, request));
- };
- // Set the headers.
- var contentType;
- if (options.headers) {
- for (var key in options.headers) {
- var value = options.headers[key];
- if (goog.isDefAndNotNull(value)) {
- request.setRequestHeader(key, value);
- }
- }
- contentType = options.headers[xhr.CONTENT_TYPE_HEADER];
- }
- // Browsers will automatically set the content type to multipart/form-data
- // when passed a FormData object.
- var dataIsFormData =
- (goog.global['FormData'] && (data instanceof goog.global['FormData']));
- // If a content type hasn't been set, it hasn't been explicitly set to null,
- // and the data isn't a FormData, 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' && contentType === undefined && !dataIsFormData) {
- request.setRequestHeader(xhr.CONTENT_TYPE_HEADER, xhr.FORM_CONTENT_TYPE);
- }
- // Set whether to include cookies with cross-domain requests. See:
- // http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
- if (options.withCredentials) {
- request.withCredentials = options.withCredentials;
- }
- // Allows setting an alternative response type, such as an ArrayBuffer. See:
- // http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype
- if (options.responseType) {
- request.responseType = options.responseType;
- }
- // Allow the request to override the MIME type of the response. See:
- // http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype
- if (options.mimeType) {
- request.overrideMimeType(options.mimeType);
- }
- // Handle timeouts, if requested.
- if (options.timeoutMs > 0) {
- timer = goog.global.setTimeout(function() {
- // Clear event listener before aborting so the errback will not be
- // called twice.
- request.onreadystatechange = goog.nullFunction;
- request.abort();
- reject(new xhr.TimeoutError(url, request));
- }, options.timeoutMs);
- }
- // Trigger the send.
- try {
- request.send(data);
- } catch (e) {
- // XMLHttpRequest.send is known to throw on some versions of FF,
- // for example if a cross-origin request is disallowed.
- request.onreadystatechange = goog.nullFunction;
- goog.global.clearTimeout(timer);
- reject(new xhr.Error('Error sending XHR: ' + e.message, url, request));
- }
- });
- };
- /**
- * @param {string} url The URL to test.
- * @return {boolean} Whether the effective scheme is HTTP or HTTPs.
- * @private
- */
- xhr.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 == '';
- };
- /**
- * JSON-parses the given response text, returning an Object.
- *
- * @param {string} responseText Response text.
- * @param {xhr.Options|undefined} options The options object.
- * @return {Object} The JSON-parsed value of the original responseText.
- * @private
- */
- xhr.parseJson_ = function(responseText, options) {
- var prefixStrippedResult = responseText;
- if (options && options.xssiPrefix) {
- prefixStrippedResult =
- xhr.stripXssiPrefix_(options.xssiPrefix, prefixStrippedResult);
- }
- return goog.json.parse(prefixStrippedResult);
- };
- /**
- * 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
- */
- xhr.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 {!goog.net.XhrLike.OrNative} request The XHR that failed.
- * @extends {goog.debug.Error}
- * @constructor
- */
- xhr.Error = function(message, url, request) {
- xhr.Error.base(this, 'constructor', message + ', url=' + url);
- /**
- * The URL that was requested.
- * @type {string}
- */
- this.url = url;
- /**
- * The XMLHttpRequest corresponding with the failed request.
- * @type {!goog.net.XhrLike.OrNative}
- */
- this.xhr = request;
- };
- goog.inherits(xhr.Error, goog.debug.Error);
- /** @override */
- xhr.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 {!goog.net.XhrLike.OrNative} request The XHR that failed.
- * @extends {xhr.Error}
- * @constructor
- * @final
- */
- xhr.HttpError = function(status, url, request) {
- xhr.HttpError.base(
- this, 'constructor', 'Request Failed, status=' + status, url, request);
- /**
- * The HTTP status code for the error.
- * @type {number}
- */
- this.status = status;
- };
- goog.inherits(xhr.HttpError, xhr.Error);
- /** @override */
- xhr.HttpError.prototype.name = 'XhrHttpError';
- /**
- * Class for Timeout errors.
- *
- * @param {string} url The URL that timed out.
- * @param {!goog.net.XhrLike.OrNative} request The XHR that failed.
- * @extends {xhr.Error}
- * @constructor
- * @final
- */
- xhr.TimeoutError = function(url, request) {
- xhr.TimeoutError.base(this, 'constructor', 'Request timed out', url, request);
- };
- goog.inherits(xhr.TimeoutError, xhr.Error);
- /** @override */
- xhr.TimeoutError.prototype.name = 'XhrTimeoutError';
- }); // goog.scope
|