469 lines
15 KiB
JavaScript
469 lines
15 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.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.debug.Error');
|
|
goog.require('goog.json');
|
|
goog.require('goog.net.HttpStatus');
|
|
goog.require('goog.net.XmlHttp');
|
|
goog.require('goog.string');
|
|
goog.require('goog.uri.utils');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
goog.scope(function() {
|
|
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|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 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. The
|
|
* responseType will be overwritten to 'arraybuffer' if it was set.
|
|
* @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) {
|
|
if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
|
|
throw new Error('getBytes is not supported in this browser.');
|
|
}
|
|
|
|
var options = 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
|