Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(345)

Side by Side Diff: remoting/webapp/crd/js/xhr.js

Issue 1028683004: Added better error and OAuth support in xhr.js. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview 6 * @fileoverview
7 * Utility class for making XHRs more pleasant. 7 * Utility class for making XHRs more pleasant.
8 */ 8 */
9 9
10 'use strict'; 10 'use strict';
11 11
12 /** @suppress {duplicate} */ 12 /** @suppress {duplicate} */
13 var remoting = remoting || {}; 13 var remoting = remoting || {};
14 14
15 /** 15 /**
16 * @constructor 16 * @constructor
17 * @param {remoting.Xhr.Params} params 17 * @param {remoting.Xhr.Params} params
18 */ 18 */
19 remoting.Xhr = function(params) { 19 remoting.Xhr = function(params) {
20 /** @private @const {!XMLHttpRequest} */ 20 remoting.Xhr.checkParams_(params);
21 this.nativeXhr_ = new XMLHttpRequest();
22 this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this);
23 this.nativeXhr_.withCredentials = params.withCredentials || false;
24 21
25 /** @private @const */ 22 /** @private @const */
26 this.responseType_ = params.responseType || remoting.Xhr.ResponseType.TEXT; 23 this.ignoreErrors_ = params.ignoreErrors || null;
27 24
28 // Apply URL parameters. 25 // Apply URL parameters.
29 var url = params.url; 26 var url = params.url;
30 var parameterString = ''; 27 var parameterString = '';
31 if (typeof(params.urlParams) === 'string') { 28 if (typeof(params.urlParams) === 'string') {
32 parameterString = params.urlParams; 29 parameterString = params.urlParams;
33 } else if (typeof(params.urlParams) === 'object') { 30 } else if (typeof(params.urlParams) === 'object') {
34 parameterString = remoting.Xhr.urlencodeParamHash( 31 parameterString = remoting.Xhr.urlencodeParamHash(
35 remoting.Xhr.removeNullFields_(params.urlParams)); 32 remoting.Xhr.removeNullFields_(params.urlParams));
36 } 33 }
37 if (parameterString) { 34 if (parameterString) {
38 base.debug.assert(url.indexOf('?') == -1);
39 url += '?' + parameterString; 35 url += '?' + parameterString;
40 } 36 }
41 37
42 // Check that the content spec is consistent.
43 if ((Number(params.textContent !== undefined) +
44 Number(params.formContent !== undefined) +
45 Number(params.jsonContent !== undefined)) > 1) {
46 throw new Error(
47 'may only specify one of textContent, formContent, and jsonContent');
48 }
49
50 // Prepare the build modified headers. 38 // Prepare the build modified headers.
51 var headers = remoting.Xhr.removeNullFields_(params.headers); 39 /** @const */
40 this.headers_ = remoting.Xhr.removeNullFields_(params.headers);
52 41
53 // Convert the content fields to a single text content variable. 42 // Convert the content fields to a single text content variable.
54 /** @private {?string} */ 43 /** @private {?string} */
55 this.content_ = null; 44 this.content_ = null;
56 if (params.textContent !== undefined) { 45 if (params.textContent !== undefined) {
46 this.maybeSetContentType_('text/plain');
57 this.content_ = params.textContent; 47 this.content_ = params.textContent;
58 } else if (params.formContent !== undefined) { 48 } else if (params.formContent !== undefined) {
59 if (!('Content-type' in headers)) { 49 this.maybeSetContentType_('application/x-www-form-urlencoded');
60 headers['Content-type'] = 'application/x-www-form-urlencoded';
61 }
62 this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent); 50 this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent);
63 } else if (params.jsonContent !== undefined) { 51 } else if (params.jsonContent !== undefined) {
64 if (!('Content-type' in headers)) { 52 this.maybeSetContentType_('application/json');
65 headers['Content-type'] = 'application/json; charset=UTF-8';
66 }
67 this.content_ = JSON.stringify(params.jsonContent); 53 this.content_ = JSON.stringify(params.jsonContent);
68 } 54 }
69 55
70 // Apply the oauthToken field. 56 // Apply the oauthToken field.
71 if (params.oauthToken !== undefined) { 57 if (params.oauthToken !== undefined) {
72 base.debug.assert(!('Authorization' in headers)); 58 this.setAuthToken_(params.oauthToken);
73 headers['Authorization'] = 'Bearer ' + params.oauthToken;
74 } 59 }
75 60
61 /** @private @const {boolean} */
62 this.acceptJson_ = params.acceptJson || false;
63 if (this.acceptJson_) {
64 this.maybeSetHeader_('Accept', 'application/json');
65 }
66
67 // Apply useIdentity field.
68 /** @const {boolean} */
69 this.useIdentity_ = params.useIdentity || false;
70
71 /** @private @const {!XMLHttpRequest} */
72 this.nativeXhr_ = new XMLHttpRequest();
73 this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this);
74 this.nativeXhr_.withCredentials = params.withCredentials || false;
76 this.nativeXhr_.open(params.method, url, true); 75 this.nativeXhr_.open(params.method, url, true);
77 for (var key in headers) {
78 this.nativeXhr_.setRequestHeader(key, headers[key]);
79 }
80 76
81 /** @private {base.Deferred<!remoting.Xhr.Response>} */ 77 /** @private {base.Deferred<!remoting.Xhr.Response>} */
82 this.deferred_ = null; 78 this.deferred_ = null;
79
80 /** @private {boolean} True if the request has been aborted. */
81 this.aborted_ = false;
Jamie 2015/03/23 23:11:33 Do you need to distinguish between this case and |
John Williams 2015/03/24 00:52:12 Yes, when useIdentity is true. Although I didn't
Jamie 2015/03/24 19:44:11 I don't understand why you need it when useIdentit
John Williams 2015/03/24 23:34:40 It's moot since I got rid of the method, but be ca
82
83 }; 83 };
84 84
85 /** 85 /**
86 * @enum {string} 86 * Parameters for the 'start' function. Unless otherwise noted, all
87 */ 87 * parameters are optional.
88 remoting.Xhr.ResponseType = {
89 TEXT: 'TEXT', // Request a plain text response (default).
90 JSON: 'JSON', // Request a JSON response.
91 NONE: 'NONE' // Don't request any response.
92 };
93
94 /**
95 * Parameters for the 'start' function.
96 * 88 *
97 * method: The HTTP method to use. 89 * method: (required) The HTTP method to use.
98 * 90 *
99 * url: The URL to request. 91 * url: (required) The URL to request.
100 * 92 *
101 * urlParams: (optional) Parameters to be appended to the URL. 93 * urlParams: Parameters to be appended to the URL. Null-valued
102 * Null-valued parameters are omitted. 94 * parameters are omitted.
103 * 95 *
104 * textContent: (optional) Text to be sent as the request body. 96 * textContent: Text to be sent as the request body.
105 * 97 *
106 * formContent: (optional) Data to be URL-encoded and sent as the 98 * formContent: Data to be URL-encoded and sent as the request body.
107 * request body. Causes Content-type header to be set 99 * Causes Content-type header to be set appropriately.
108 * appropriately.
109 * 100 *
110 * jsonContent: (optional) Data to be JSON-encoded and sent as the 101 * jsonContent: Data to be JSON-encoded and sent as the request body.
111 * request body. Causes Content-type header to be set 102 * Causes Content-type header to be set appropriately.
112 * appropriately.
113 * 103 *
114 * headers: (optional) Additional request headers to be sent. 104 * headers: Additional request headers to be sent. Null-valued
115 * Null-valued headers are omitted. 105 * headers are omitted.
116 * 106 *
117 * withCredentials: (optional) Value of the XHR's withCredentials field. 107 * withCredentials: Value of the XHR's withCredentials field.
118 * 108 *
119 * oauthToken: (optional) An OAuth2 token used to construct an 109 * oauthToken: An OAuth2 token used to construct an Authentication
120 * Authentication header. 110 * header.
121 * 111 *
122 * responseType: (optional) Request a response of a specific 112 * useIdentity: Use identity API to get an OAuth2 token.
123 * type. Default: TEXT. 113 *
114 * ignoreErrors: List of error types (arising from HTTP result codes)
115 * to ignore. If null (the default) no HTTP status code is
116 * treated as an error.
Jamie 2015/03/23 23:11:34 This seems like an odd thing to put into a general
John Williams 2015/03/24 00:52:12 Done.
117 *
118 * acceptJson: If true, send an Accept header indicating that a JSON
119 * response is expected.
124 * 120 *
125 * @typedef {{ 121 * @typedef {{
126 * method: string, 122 * method: string,
127 * url:string, 123 * url:string,
128 * urlParams:(string|Object<string,?string>|undefined), 124 * urlParams:(string|Object<string,?string>|undefined),
129 * textContent:(string|undefined), 125 * textContent:(string|undefined),
130 * formContent:(Object|undefined), 126 * formContent:(Object|undefined),
131 * jsonContent:(*|undefined), 127 * jsonContent:(*|undefined),
132 * headers:(Object<string,?string>|undefined), 128 * headers:(Object<string,?string>|undefined),
133 * withCredentials:(boolean|undefined), 129 * withCredentials:(boolean|undefined),
134 * oauthToken:(string|undefined), 130 * oauthToken:(string|undefined),
135 * responseType:(remoting.Xhr.ResponseType|undefined) 131 * useIdentity:(boolean|undefined),
132 * ignoreErrors:(Array<remoting.Error.Tag>|undefined),
133 * acceptJson:(boolean|undefined)
136 * }} 134 * }}
137 */ 135 */
138 remoting.Xhr.Params; 136 remoting.Xhr.Params;
139 137
140 /** 138 /**
141 * Aborts the HTTP request. Does nothing is the request has finished 139 * Aborts the HTTP request. Does nothing if the request has finished
142 * already. 140 * already. Prevents the promise returned by start() from ever being
141 * resolved or rejected.
Jamie 2015/03/23 23:11:34 Is that the right contract for this call? Generall
John Williams 2015/03/24 00:52:12 So far, this method has exactly one call site, and
Jamie 2015/03/24 19:44:11 What's the call-site? I don't see it in the diffs,
John Williams 2015/03/24 23:34:40 It was API compatible, but you should see it now t
143 */ 142 */
144 remoting.Xhr.prototype.abort = function() { 143 remoting.Xhr.prototype.abort = function() {
145 this.nativeXhr_.abort(); 144 this.aborted_ = true;
145 try {
146 this.nativeXhr_.abort();
147 } catch (error) {
148 // This abort method throws an exception if called before the
149 // request is sent. This isn't an error for our purposes, so we
150 // just ignore the exception.
151 }
146 }; 152 };
147 153
148 /** 154 /**
149 * Starts and HTTP request and gets a promise that is resolved when 155 * Starts and HTTP request and gets a promise that is resolved when
150 * the request completes. 156 * the request completes.
151 * 157 *
152 * Any error that prevents receiving an HTTP status 158 * Any error that prevents receiving an HTTP status
153 * code causes this promise to be rejected. 159 * code causes this promise to be rejected.
154 * 160 *
155 * NOTE: Calling this method more than once will return the same 161 * NOTE: Calling this method more than once will return the same
156 * promise and not start a new request, despite what the name 162 * promise and not start a new request, despite what the name
157 * suggests. 163 * suggests.
158 * 164 *
159 * @return {!Promise<!remoting.Xhr.Response>} 165 * @return {!Promise<!remoting.Xhr.Response>}
160 */ 166 */
161 remoting.Xhr.prototype.start = function() { 167 remoting.Xhr.prototype.start = function() {
162 if (this.deferred_ == null) { 168 if (this.deferred_ == null) {
163 var xhr = this.nativeXhr_;
164 xhr.send(this.content_);
165 this.content_ = null; // for gc
166 this.deferred_ = new base.Deferred(); 169 this.deferred_ = new base.Deferred();
170
171 // Send the XHR, possibly after getting an OAuth token.
172 var self = this;
Jamie 2015/03/23 23:11:33 We've tended to use |that|, rather than |self| whe
John Williams 2015/03/24 00:52:12 Done, but I'm not a big fan because in ordinary us
173 if (this.useIdentity_) {
174 remoting.identity.getToken().then(function(token) {
175 base.debug.assert(self.nativeXhr_.readyState == 1);
176 self.setAuthToken_(token);
177 self.sendXhr_();
178 }, function(error) {
Jamie 2015/03/23 23:11:33 Please use the more explicit catch() method, rathe
John Williams 2015/03/24 00:52:12 OK. It makes no difference here but I should poin
Jamie 2015/03/24 19:44:11 Thanks for the clarification. I wasn't aware of th
179 if (!self.aborted_) {
180 self.deferred_.reject(error);
181 }
182 });
183 } else {
184 this.sendXhr_();
185 }
167 } 186 }
168 return this.deferred_.promise(); 187 return this.deferred_.promise();
169 }; 188 };
170 189
171 /** 190 /**
191 * @param {remoting.Xhr.Params} params
192 * @throws {Error} if params are invalid
193 */
194 remoting.Xhr.checkParams_ = function(params) {
195 if (params.urlParams) {
196 if (params.url.indexOf('?') != -1) {
197 throw new Error('URL may not contain "?" when urlParams is set');
198 }
199 if (params.url.indexOf('#') != -1) {
200 throw new Error('URL may not contain "#" when urlParams is set');
201 }
202 }
203
204 if ((Number(params.textContent !== undefined) +
205 Number(params.formContent !== undefined) +
206 Number(params.jsonContent !== undefined)) > 1) {
207 throw new Error(
208 'may only specify one of textContent, formContent, and jsonContent');
209 }
210
211 if (params.useIdentity && params.oauthToken !== undefined) {
212 throw new Error('may not specify both useIdentity and oauthToken');
213 }
214
215 if ((params.useIdentity || params.oauthToken !== undefined) &&
216 params.headers &&
217 params.headers['Authorization'] != null) {
218 throw new Error(
219 'may not specify useIdentity or oauthToken ' +
220 'with an Authorization header');
221 }
222 };
223
224 /**
225 * @param {string} token
226 * @private
227 */
228 remoting.Xhr.prototype.setAuthToken_ = function(token) {
229 this.setHeader_('Authorization', 'Bearer ' + token);
230 };
231
232 /**
233 * @param {string} type
234 * @private
235 */
236 remoting.Xhr.prototype.maybeSetContentType_ = function(type) {
237 this.maybeSetHeader_('Content-type', type + '; charset=UTF-8');
238 };
239
240 /**
241 * @param {string} key
242 * @param {string} value
243 * @private
244 */
245 remoting.Xhr.prototype.setHeader_ = function(key, value) {
246 var wasSet = this.maybeSetHeader_(key, value);
247 base.debug.assert(wasSet);
248 };
249
250 /**
251 * @param {string} key
252 * @param {string} value
253 * @return {boolean}
254 * @private
255 */
256 remoting.Xhr.prototype.maybeSetHeader_ = function(key, value) {
257 if (!(key in this.headers_)) {
258 this.headers_[key] = value;
259 return true;
260 }
261 return false;
262 };
263
264 /** @private */
265 remoting.Xhr.prototype.sendXhr_ = function() {
266 if (!this.aborted_) {
267 for (var key in this.headers_) {
268 this.nativeXhr_.setRequestHeader(key, this.headers_[key]);
269 }
270 this.nativeXhr_.send(this.content_);
271 }
272 this.content_ = null; // for gc
273 };
274
275 /**
172 * @private 276 * @private
173 */ 277 */
174 remoting.Xhr.prototype.onReadyStateChange_ = function() { 278 remoting.Xhr.prototype.onReadyStateChange_ = function() {
175 var xhr = this.nativeXhr_; 279 var xhr = this.nativeXhr_;
176 if (xhr.readyState == 4) { 280 if (xhr.readyState == 4 && !this.aborted_) {
177 // See comments at remoting.Xhr.Response. 281 var error = remoting.Error.fromHttpStatus(xhr.status);
Jamie 2015/03/23 23:11:34 I don't think that this is the right place to do t
John Williams 2015/03/24 00:52:12 I'm not sure I agree, but I'll compromise and just
178 this.deferred_.resolve(new remoting.Xhr.Response(xhr, this.responseType_)); 282 if (error.isNone() ||
283 this.ignoreErrors_ == null ||
284 error.hasTag.apply(error, this.ignoreErrors_)) {
285 // See comments at remoting.Xhr.Response.
286 this.deferred_.resolve(new remoting.Xhr.Response(
287 xhr, this.acceptJson_));
288 } else {
289 this.deferred_.reject(error);
290 }
179 } 291 }
180 }; 292 };
181 293
182 /** 294 /**
183 * The response-related parts of an XMLHttpRequest. Note that this 295 * The response-related parts of an XMLHttpRequest. Note that this
184 * class is not just a facade for XMLHttpRequest; it saves the value 296 * class is not just a facade for XMLHttpRequest; it saves the value
185 * of the |responseText| field becuase once onReadyStateChange_ 297 * of the |responseText| field becuase once onReadyStateChange_
186 * (above) returns, the value of |responseText| is reset to the empty 298 * (above) returns, the value of |responseText| is reset to the empty
187 * string! This is a documented anti-feature of the XMLHttpRequest 299 * string! This is a documented anti-feature of the XMLHttpRequest
188 * API. 300 * API.
189 * 301 *
190 * @constructor 302 * @constructor
191 * @param {!XMLHttpRequest} xhr 303 * @param {!XMLHttpRequest} xhr
192 * @param {remoting.Xhr.ResponseType} type 304 * @param {boolean} allowJson
193 */ 305 */
194 remoting.Xhr.Response = function(xhr, type) { 306 remoting.Xhr.Response = function(xhr, allowJson) {
195 /** @private @const */ 307 /** @private @const */
196 this.type_ = type; 308 this.allowJson_ = allowJson;
197 309
198 /** 310 /**
199 * The HTTP status code. 311 * The HTTP status code.
200 * @const {number} 312 * @const {number}
201 */ 313 */
202 this.status = xhr.status; 314 this.status = xhr.status;
203 315
204 /** 316 /**
205 * The HTTP status description. 317 * The HTTP status description.
206 * @const {string} 318 * @const {string}
207 */ 319 */
208 this.statusText = xhr.statusText; 320 this.statusText = xhr.statusText;
209 321
210 /** 322 /**
211 * The response URL, if any. 323 * The response URL, if any.
212 * @const {?string} 324 * @const {?string}
213 */ 325 */
214 this.url = xhr.responseURL; 326 this.url = xhr.responseURL;
215 327
216 /** @private {string} */ 328 /** @private {string} */
217 this.text_ = xhr.responseText || ''; 329 this.text_ = xhr.responseText || '';
330
331 /** @private {*|undefined} */
332 this.json_ = undefined;
218 }; 333 };
219 334
220 /** 335 /**
221 * @return {string} The text content of the response. 336 * @return {string} The text content of the response.
222 */ 337 */
223 remoting.Xhr.Response.prototype.getText = function() { 338 remoting.Xhr.Response.prototype.getText = function() {
224 return this.text_; 339 return this.text_;
225 }; 340 };
226 341
227 /** 342 /**
343 * Get the JSON content of the response. Requires acceptJson to have
344 * been true in the request.
228 * @return {*} The parsed JSON content of the response. 345 * @return {*} The parsed JSON content of the response.
229 */ 346 */
230 remoting.Xhr.Response.prototype.getJson = function() { 347 remoting.Xhr.Response.prototype.getJson = function() {
231 base.debug.assert(this.type_ == remoting.Xhr.ResponseType.JSON); 348 base.debug.assert(this.allowJson_);
232 return JSON.parse(this.text_); 349 if (this.json_ === undefined) {
350 this.json_ = JSON.parse(this.text_);
351 }
352 return this.json_;
233 }; 353 };
234 354
235 /** 355 /**
236 * Returns a copy of the input object with all null or undefined 356 * Returns a copy of the input object with all null or undefined
237 * fields removed. 357 * fields removed.
238 * 358 *
239 * @param {Object<string,?string>|undefined} input 359 * @param {Object<string,?string>|undefined} input
240 * @return {!Object<string,string>} 360 * @return {!Object<string,string>}
241 * @private 361 * @private
242 */ 362 */
(...skipping 21 matching lines...) Expand all
264 var paramArray = []; 384 var paramArray = [];
265 for (var key in paramHash) { 385 for (var key in paramHash) {
266 paramArray.push(encodeURIComponent(key) + 386 paramArray.push(encodeURIComponent(key) +
267 '=' + encodeURIComponent(paramHash[key])); 387 '=' + encodeURIComponent(paramHash[key]));
268 } 388 }
269 if (paramArray.length > 0) { 389 if (paramArray.length > 0) {
270 return paramArray.join('&'); 390 return paramArray.join('&');
271 } 391 }
272 return ''; 392 return '';
273 }; 393 };
274
275 /**
276 * Generic success/failure response proxy.
277 *
278 * TODO(jrw): Stop using this and move default error handling directly
279 * into Xhr class.
280 *
281 * @param {function():void} onDone
282 * @param {function(!remoting.Error):void} onError
283 * @param {Array<remoting.Error.Tag>=} opt_ignoreErrors
284 * @return {function(!remoting.Xhr.Response):void}
285 */
286 remoting.Xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) {
287 /** @param {!remoting.Xhr.Response} response */
288 var result = function(response) {
289 var error = remoting.Error.fromHttpStatus(response.status);
290 if (error.isNone()) {
291 onDone();
292 return;
293 }
294
295 if (opt_ignoreErrors && error.hasTag.apply(error, opt_ignoreErrors)) {
296 onDone();
297 return;
298 }
299
300 onError(error);
301 };
302 return result;
303 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698