Path: blob/trunk/third_party/closure/goog/labs/net/xhr.js
2868 views
// Copyright 2011 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.131415/**16* @fileoverview Offered as an alternative to XhrIo as a way for making requests17* via XMLHttpRequest. Instead of mirroring the XHR interface and exposing18* events, results are used as a way to pass a "promise" of the response to19* interested parties.20*21*/2223goog.provide('goog.labs.net.xhr');24goog.provide('goog.labs.net.xhr.Error');25goog.provide('goog.labs.net.xhr.HttpError');26goog.provide('goog.labs.net.xhr.Options');27goog.provide('goog.labs.net.xhr.PostData');28goog.provide('goog.labs.net.xhr.ResponseType');29goog.provide('goog.labs.net.xhr.TimeoutError');3031goog.require('goog.Promise');32goog.require('goog.asserts');33goog.require('goog.debug.Error');34goog.require('goog.json');35goog.require('goog.net.HttpStatus');36goog.require('goog.net.XmlHttp');37goog.require('goog.object');38goog.require('goog.string');39goog.require('goog.uri.utils');40goog.require('goog.userAgent');41424344goog.scope(function() {45var userAgent = goog.userAgent;46var xhr = goog.labs.net.xhr;47var HttpStatus = goog.net.HttpStatus;484950/**51* Configuration options for an XMLHttpRequest.52* - headers: map of header key/value pairs.53* - timeoutMs: number of milliseconds after which the request will be timed54* out by the client. Default is to allow the browser to handle timeouts.55* - withCredentials: whether user credentials are to be included in a56* cross-origin request. See:57* http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute58* - mimeType: allows the caller to override the content-type and charset for59* the request. See:60* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype61* - responseType: may be set to change the response type to an arraybuffer or62* blob for downloading binary data. See:63* http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype]64* - xmlHttpFactory: allows the caller to override the factory used to create65* XMLHttpRequest objects.66* - xssiPrefix: Prefix used for protecting against XSSI attacks, which should67* be removed before parsing the response as JSON.68*69* @typedef {{70* headers: (Object<string>|undefined),71* mimeType: (string|undefined),72* responseType: (xhr.ResponseType|undefined),73* timeoutMs: (number|undefined),74* withCredentials: (boolean|undefined),75* xmlHttpFactory: (goog.net.XmlHttpFactory|undefined),76* xssiPrefix: (string|undefined)77* }}78*/79xhr.Options;808182/**83* Defines the types that are allowed as post data.84* @typedef {(ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string|undefined)}85*/86xhr.PostData;878889/**90* The Content-Type HTTP header name.91* @type {string}92*/93xhr.CONTENT_TYPE_HEADER = 'Content-Type';949596/**97* The Content-Type HTTP header value for a url-encoded form.98* @type {string}99*/100xhr.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';101102103/**104* Supported data types for the responseType field.105* See: http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-response106* @enum {string}107*/108xhr.ResponseType = {109ARRAYBUFFER: 'arraybuffer',110BLOB: 'blob',111DOCUMENT: 'document',112JSON: 'json',113TEXT: 'text'114};115116117/**118* Sends a get request, returning a promise that will be resolved119* with the response text once the request completes.120*121* @param {string} url The URL to request.122* @param {xhr.Options=} opt_options Configuration options for the request.123* @return {!goog.Promise<string>} A promise that will be resolved with the124* response text once the request completes.125*/126xhr.get = function(url, opt_options) {127return xhr.send('GET', url, null, opt_options).then(function(request) {128return request.responseText;129});130};131132133/**134* Sends a post request, returning a promise that will be resolved135* with the response text once the request completes.136*137* @param {string} url The URL to request.138* @param {xhr.PostData} data The body of the post request.139* @param {xhr.Options=} opt_options Configuration options for the request.140* @return {!goog.Promise<string>} A promise that will be resolved with the141* response text once the request completes.142*/143xhr.post = function(url, data, opt_options) {144return xhr.send('POST', url, data, opt_options).then(function(request) {145return request.responseText;146});147};148149150/**151* Sends a get request, returning a promise that will be resolved with152* the parsed response text once the request completes.153*154* @param {string} url The URL to request.155* @param {xhr.Options=} opt_options Configuration options for the request.156* @return {!goog.Promise<Object>} A promise that will be resolved with the157* response JSON once the request completes.158*/159xhr.getJson = function(url, opt_options) {160return xhr.send('GET', url, null, opt_options).then(function(request) {161return xhr.parseJson_(request.responseText, opt_options);162});163};164165166/**167* Sends a get request, returning a promise that will be resolved with the168* response as a Blob.169*170* @param {string} url The URL to request.171* @param {xhr.Options=} opt_options Configuration options for the request. If172* responseType is set, it will be ignored for this request.173* @return {!goog.Promise<!Blob>} A promise that will be resolved with an174* immutable Blob representing the file once the request completes.175*/176xhr.getBlob = function(url, opt_options) {177goog.asserts.assert(178'Blob' in goog.global, 'getBlob is not supported in this browser.');179180var options = opt_options ? goog.object.clone(opt_options) : {};181options.responseType = xhr.ResponseType.BLOB;182183return xhr.send('GET', url, null, options).then(function(request) {184return /** @type {!Blob} */ (request.response);185});186};187188189/**190* Sends a get request, returning a promise that will be resolved with the191* response as an array of bytes.192*193* Supported in all XMLHttpRequest level 2 browsers, as well as IE9. IE8 and194* earlier are not supported.195*196* @param {string} url The URL to request.197* @param {xhr.Options=} opt_options Configuration options for the request. If198* responseType is set, it will be ignored for this request.199* @return {!goog.Promise<!Uint8Array|!Array<number>>} A promise that will be200* resolved with an array of bytes once the request completes.201*/202xhr.getBytes = function(url, opt_options) {203goog.asserts.assert(204!userAgent.IE || userAgent.isDocumentModeOrHigher(9),205'getBytes is not supported in this browser.');206207var options = opt_options ? goog.object.clone(opt_options) : {};208options.responseType = xhr.ResponseType.ARRAYBUFFER;209210return xhr.send('GET', url, null, options).then(function(request) {211// Use the ArrayBuffer response in browsers that support XMLHttpRequest2.212// This covers nearly all modern browsers: http://caniuse.com/xhr2213if (request.response) {214return new Uint8Array(/** @type {!ArrayBuffer} */ (request.response));215}216217// Fallback for IE9: the response may be accessed as an array of bytes with218// the non-standard responseBody property, which can only be accessed as a219// VBArray. IE7 and IE8 require significant amounts of VBScript to extract220// the bytes.221// See: http://stackoverflow.com/questions/1919972/222if (goog.global['VBArray']) {223return new goog.global['VBArray'](request['responseBody']).toArray();224}225226// Nearly all common browsers are covered by the cases above. If downloading227// binary files in older browsers is necessary, the MDN article "Sending and228// Receiving Binary Data" provides techniques that may work with229// XMLHttpRequest level 1 browsers: http://goo.gl/7lEuGN230throw new xhr.Error(231'getBytes is not supported in this browser.', url, request);232});233};234235236/**237* Sends a post request, returning a promise that will be resolved with238* the parsed response text once the request completes.239*240* @param {string} url The URL to request.241* @param {xhr.PostData} data The body of the post request.242* @param {xhr.Options=} opt_options Configuration options for the request.243* @return {!goog.Promise<Object>} A promise that will be resolved with the244* response JSON once the request completes.245*/246xhr.postJson = function(url, data, opt_options) {247return xhr.send('POST', url, data, opt_options).then(function(request) {248return xhr.parseJson_(request.responseText, opt_options);249});250};251252253/**254* Sends a request, returning a promise that will be resolved255* with the XHR object once the request completes.256*257* If content type hasn't been set in opt_options headers, and hasn't been258* explicitly set to null, default to form-urlencoded/UTF8 for POSTs.259*260* @param {string} method The HTTP method for the request.261* @param {string} url The URL to request.262* @param {xhr.PostData} data The body of the post request.263* @param {xhr.Options=} opt_options Configuration options for the request.264* @return {!goog.Promise<!goog.net.XhrLike.OrNative>} A promise that will be265* resolved with the XHR object once the request completes.266*/267xhr.send = function(method, url, data, opt_options) {268return new goog.Promise(function(resolve, reject) {269var options = opt_options || {};270var timer;271272var request = options.xmlHttpFactory ?273options.xmlHttpFactory.createInstance() :274goog.net.XmlHttp();275try {276request.open(method, url, true);277} catch (e) {278// XMLHttpRequest.open may throw when 'open' is called, for example, IE7279// throws "Access Denied" for cross-origin requests.280reject(new xhr.Error('Error opening XHR: ' + e.message, url, request));281}282283// So sad that IE doesn't support onload and onerror.284request.onreadystatechange = function() {285if (request.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {286goog.global.clearTimeout(timer);287// Note: When developing locally, XHRs to file:// schemes return288// a status code of 0. We mark that case as a success too.289if (HttpStatus.isSuccess(request.status) ||290request.status === 0 && !xhr.isEffectiveSchemeHttp_(url)) {291resolve(request);292} else {293reject(new xhr.HttpError(request.status, url, request));294}295}296};297request.onerror = function() {298reject(new xhr.Error('Network error', url, request));299};300301// Set the headers.302var contentType;303if (options.headers) {304for (var key in options.headers) {305var value = options.headers[key];306if (goog.isDefAndNotNull(value)) {307request.setRequestHeader(key, value);308}309}310contentType = options.headers[xhr.CONTENT_TYPE_HEADER];311}312313// Browsers will automatically set the content type to multipart/form-data314// when passed a FormData object.315var dataIsFormData =316(goog.global['FormData'] && (data instanceof goog.global['FormData']));317// If a content type hasn't been set, it hasn't been explicitly set to null,318// and the data isn't a FormData, default to form-urlencoded/UTF8 for POSTs.319// This is because some proxies have been known to reject posts without a320// content-type.321if (method == 'POST' && contentType === undefined && !dataIsFormData) {322request.setRequestHeader(xhr.CONTENT_TYPE_HEADER, xhr.FORM_CONTENT_TYPE);323}324325// Set whether to include cookies with cross-domain requests. See:326// http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute327if (options.withCredentials) {328request.withCredentials = options.withCredentials;329}330331// Allows setting an alternative response type, such as an ArrayBuffer. See:332// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-responsetype333if (options.responseType) {334request.responseType = options.responseType;335}336337// Allow the request to override the MIME type of the response. See:338// http://www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-overridemimetype339if (options.mimeType) {340request.overrideMimeType(options.mimeType);341}342343// Handle timeouts, if requested.344if (options.timeoutMs > 0) {345timer = goog.global.setTimeout(function() {346// Clear event listener before aborting so the errback will not be347// called twice.348request.onreadystatechange = goog.nullFunction;349request.abort();350reject(new xhr.TimeoutError(url, request));351}, options.timeoutMs);352}353354// Trigger the send.355try {356request.send(data);357} catch (e) {358// XMLHttpRequest.send is known to throw on some versions of FF,359// for example if a cross-origin request is disallowed.360request.onreadystatechange = goog.nullFunction;361goog.global.clearTimeout(timer);362reject(new xhr.Error('Error sending XHR: ' + e.message, url, request));363}364});365};366367368/**369* @param {string} url The URL to test.370* @return {boolean} Whether the effective scheme is HTTP or HTTPs.371* @private372*/373xhr.isEffectiveSchemeHttp_ = function(url) {374var scheme = goog.uri.utils.getEffectiveScheme(url);375// NOTE(user): Empty-string is for the case under FF3.5 when the location376// is not defined inside a web worker.377return scheme == 'http' || scheme == 'https' || scheme == '';378};379380381/**382* JSON-parses the given response text, returning an Object.383*384* @param {string} responseText Response text.385* @param {xhr.Options|undefined} options The options object.386* @return {Object} The JSON-parsed value of the original responseText.387* @private388*/389xhr.parseJson_ = function(responseText, options) {390var prefixStrippedResult = responseText;391if (options && options.xssiPrefix) {392prefixStrippedResult =393xhr.stripXssiPrefix_(options.xssiPrefix, prefixStrippedResult);394}395return goog.json.parse(prefixStrippedResult);396};397398399/**400* Strips the XSSI prefix from the input string.401*402* @param {string} prefix The XSSI prefix.403* @param {string} string The string to strip the prefix from.404* @return {string} The input string without the prefix.405* @private406*/407xhr.stripXssiPrefix_ = function(prefix, string) {408if (goog.string.startsWith(string, prefix)) {409string = string.substring(prefix.length);410}411return string;412};413414415416/**417* Generic error that may occur during a request.418*419* @param {string} message The error message.420* @param {string} url The URL that was being requested.421* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.422* @extends {goog.debug.Error}423* @constructor424*/425xhr.Error = function(message, url, request) {426xhr.Error.base(this, 'constructor', message + ', url=' + url);427428/**429* The URL that was requested.430* @type {string}431*/432this.url = url;433434/**435* The XMLHttpRequest corresponding with the failed request.436* @type {!goog.net.XhrLike.OrNative}437*/438this.xhr = request;439};440goog.inherits(xhr.Error, goog.debug.Error);441442443/** @override */444xhr.Error.prototype.name = 'XhrError';445446447448/**449* Class for HTTP errors.450*451* @param {number} status The HTTP status code of the response.452* @param {string} url The URL that was being requested.453* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.454* @extends {xhr.Error}455* @constructor456* @final457*/458xhr.HttpError = function(status, url, request) {459xhr.HttpError.base(460this, 'constructor', 'Request Failed, status=' + status, url, request);461462/**463* The HTTP status code for the error.464* @type {number}465*/466this.status = status;467};468goog.inherits(xhr.HttpError, xhr.Error);469470471/** @override */472xhr.HttpError.prototype.name = 'XhrHttpError';473474475476/**477* Class for Timeout errors.478*479* @param {string} url The URL that timed out.480* @param {!goog.net.XhrLike.OrNative} request The XHR that failed.481* @extends {xhr.Error}482* @constructor483* @final484*/485xhr.TimeoutError = function(url, request) {486xhr.TimeoutError.base(this, 'constructor', 'Request timed out', url, request);487};488goog.inherits(xhr.TimeoutError, xhr.Error);489490491/** @override */492xhr.TimeoutError.prototype.name = 'XhrTimeoutError';493494}); // goog.scope495496497