Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/third_party/closure/goog/labs/net/webchannel/channelrequest.js
1865 views
1
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Definition of the ChannelRequest class. The request
17
* object encapsulates the logic for making a single request, either for the
18
* forward channel, back channel, or test channel, to the server. It contains
19
* the logic for the two types of transports we use:
20
* XMLHTTP and Image request. It provides timeout detection. More transports
21
* to be added in future, such as Fetch, WebSocket.
22
*
23
* @visibility {:internal}
24
*/
25
26
27
goog.provide('goog.labs.net.webChannel.ChannelRequest');
28
29
goog.require('goog.Timer');
30
goog.require('goog.async.Throttle');
31
goog.require('goog.events.EventHandler');
32
goog.require('goog.labs.net.webChannel.Channel');
33
goog.require('goog.labs.net.webChannel.WebChannelDebug');
34
goog.require('goog.labs.net.webChannel.requestStats');
35
goog.require('goog.labs.net.webChannel.requestStats.ServerReachability');
36
goog.require('goog.labs.net.webChannel.requestStats.Stat');
37
goog.require('goog.net.ErrorCode');
38
goog.require('goog.net.EventType');
39
goog.require('goog.net.XmlHttp');
40
goog.require('goog.object');
41
goog.require('goog.userAgent');
42
43
44
45
/**
46
* A new ChannelRequest is created for each request to the server.
47
*
48
* @param {goog.labs.net.webChannel.Channel} channel
49
* The channel that owns this request.
50
* @param {goog.labs.net.webChannel.WebChannelDebug} channelDebug A
51
* WebChannelDebug to use for logging.
52
* @param {string=} opt_sessionId The session id for the channel.
53
* @param {string|number=} opt_requestId The request id for this request.
54
* @param {number=} opt_retryId The retry id for this request.
55
* @constructor
56
* @struct
57
* @final
58
*/
59
goog.labs.net.webChannel.ChannelRequest = function(
60
channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
61
/**
62
* The channel object that owns the request.
63
* @private {goog.labs.net.webChannel.Channel}
64
*/
65
this.channel_ = channel;
66
67
/**
68
* The channel debug to use for logging
69
* @private {goog.labs.net.webChannel.WebChannelDebug}
70
*/
71
this.channelDebug_ = channelDebug;
72
73
/**
74
* The Session ID for the channel.
75
* @private {string|undefined}
76
*/
77
this.sid_ = opt_sessionId;
78
79
/**
80
* The RID (request ID) for the request.
81
* @private {string|number|undefined}
82
*/
83
this.rid_ = opt_requestId;
84
85
/**
86
* The attempt number of the current request.
87
* @private {number}
88
*/
89
this.retryId_ = opt_retryId || 1;
90
91
/**
92
* An object to keep track of the channel request event listeners.
93
* @private {!goog.events.EventHandler<
94
* !goog.labs.net.webChannel.ChannelRequest>}
95
*/
96
this.eventHandler_ = new goog.events.EventHandler(this);
97
98
/**
99
* The timeout in ms before failing the request.
100
* @private {number}
101
*/
102
this.timeout_ = goog.labs.net.webChannel.ChannelRequest.TIMEOUT_MS_;
103
104
/**
105
* A timer for polling responseText in browsers that don't fire
106
* onreadystatechange during incremental loading of responseText.
107
* @private {goog.Timer}
108
*/
109
this.pollingTimer_ = new goog.Timer();
110
111
this.pollingTimer_.setInterval(
112
goog.labs.net.webChannel.ChannelRequest.POLLING_INTERVAL_MS_);
113
114
/**
115
* Extra HTTP headers to add to all the requests sent to the server.
116
* @private {Object}
117
*/
118
this.extraHeaders_ = null;
119
120
121
/**
122
* Whether the request was successful. This is only set to true after the
123
* request successfully completes.
124
* @private {boolean}
125
*/
126
this.successful_ = false;
127
128
129
/**
130
* The TimerID of the timer used to detect if the request has timed-out.
131
* @type {?number}
132
* @private
133
*/
134
this.watchDogTimerId_ = null;
135
136
/**
137
* The time in the future when the request will timeout.
138
* @private {?number}
139
*/
140
this.watchDogTimeoutTime_ = null;
141
142
/**
143
* The time the request started.
144
* @private {?number}
145
*/
146
this.requestStartTime_ = null;
147
148
/**
149
* The type of request (XMLHTTP, IMG)
150
* @private {?number}
151
*/
152
this.type_ = null;
153
154
/**
155
* The base Uri for the request. The includes all the parameters except the
156
* one that indicates the retry number.
157
* @private {goog.Uri}
158
*/
159
this.baseUri_ = null;
160
161
/**
162
* The request Uri that was actually used for the most recent request attempt.
163
* @private {goog.Uri}
164
*/
165
this.requestUri_ = null;
166
167
/**
168
* The post data, if the request is a post.
169
* @private {?string}
170
*/
171
this.postData_ = null;
172
173
/**
174
* The XhrLte request if the request is using XMLHTTP
175
* @private {goog.net.XhrIo}
176
*/
177
this.xmlHttp_ = null;
178
179
/**
180
* The position of where the next unprocessed chunk starts in the response
181
* text.
182
* @private {number}
183
*/
184
this.xmlHttpChunkStart_ = 0;
185
186
/**
187
* The verb (Get or Post) for the request.
188
* @private {?string}
189
*/
190
this.verb_ = null;
191
192
/**
193
* The last error if the request failed.
194
* @private {?goog.labs.net.webChannel.ChannelRequest.Error}
195
*/
196
this.lastError_ = null;
197
198
/**
199
* The last status code received.
200
* @private {number}
201
*/
202
this.lastStatusCode_ = -1;
203
204
/**
205
* Whether to send the Connection:close header as part of the request.
206
* @private {boolean}
207
*/
208
this.sendClose_ = true;
209
210
/**
211
* Whether the request has been cancelled due to a call to cancel.
212
* @private {boolean}
213
*/
214
this.cancelled_ = false;
215
216
/**
217
* A throttle time in ms for readystatechange events for the backchannel.
218
* Useful for throttling when ready state is INTERACTIVE (partial data).
219
* If set to zero no throttle is used.
220
*
221
* See WebChannelBase.prototype.readyStateChangeThrottleMs_
222
*
223
* @private {number}
224
*/
225
this.readyStateChangeThrottleMs_ = 0;
226
227
/**
228
* The throttle for readystatechange events for the current request, or null
229
* if there is none.
230
* @private {goog.async.Throttle}
231
*/
232
this.readyStateChangeThrottle_ = null;
233
234
235
/**
236
* Whether to the result is expected to be encoded for chunking and thus
237
* requires decoding.
238
* @private {boolean}
239
*/
240
this.decodeChunks_ = false;
241
};
242
243
244
goog.scope(function() {
245
var Channel = goog.labs.net.webChannel.Channel;
246
var ChannelRequest = goog.labs.net.webChannel.ChannelRequest;
247
var requestStats = goog.labs.net.webChannel.requestStats;
248
var WebChannelDebug = goog.labs.net.webChannel.WebChannelDebug;
249
250
251
/**
252
* Default timeout in MS for a request. The server must return data within this
253
* time limit for the request to not timeout.
254
* @private {number}
255
*/
256
ChannelRequest.TIMEOUT_MS_ = 45 * 1000;
257
258
259
/**
260
* How often to poll (in MS) for changes to responseText in browsers that don't
261
* fire onreadystatechange during incremental loading of responseText.
262
* @private {number}
263
*/
264
ChannelRequest.POLLING_INTERVAL_MS_ = 250;
265
266
267
/**
268
* Enum for channel requests type
269
* @enum {number}
270
* @private
271
*/
272
ChannelRequest.Type_ = {
273
/**
274
* XMLHTTP requests.
275
*/
276
XML_HTTP: 1,
277
278
/**
279
* IMG requests.
280
*/
281
CLOSE_REQUEST: 2
282
};
283
284
285
/**
286
* Enum type for identifying an error.
287
* @enum {number}
288
*/
289
ChannelRequest.Error = {
290
/**
291
* Errors due to a non-200 status code.
292
*/
293
STATUS: 0,
294
295
/**
296
* Errors due to no data being returned.
297
*/
298
NO_DATA: 1,
299
300
/**
301
* Errors due to a timeout.
302
*/
303
TIMEOUT: 2,
304
305
/**
306
* Errors due to the server returning an unknown.
307
*/
308
UNKNOWN_SESSION_ID: 3,
309
310
/**
311
* Errors due to bad data being received.
312
*/
313
BAD_DATA: 4,
314
315
/**
316
* Errors due to the handler throwing an exception.
317
*/
318
HANDLER_EXCEPTION: 5,
319
320
/**
321
* The browser declared itself offline during the request.
322
*/
323
BROWSER_OFFLINE: 6
324
};
325
326
327
/**
328
* Returns a useful error string for debugging based on the specified error
329
* code.
330
* @param {?ChannelRequest.Error} errorCode The error code.
331
* @param {number} statusCode The HTTP status code.
332
* @return {string} The error string for the given code combination.
333
*/
334
ChannelRequest.errorStringFromCode = function(errorCode, statusCode) {
335
switch (errorCode) {
336
case ChannelRequest.Error.STATUS:
337
return 'Non-200 return code (' + statusCode + ')';
338
case ChannelRequest.Error.NO_DATA:
339
return 'XMLHTTP failure (no data)';
340
case ChannelRequest.Error.TIMEOUT:
341
return 'HttpConnection timeout';
342
default:
343
return 'Unknown error';
344
}
345
};
346
347
348
/**
349
* Sentinel value used to indicate an invalid chunk in a multi-chunk response.
350
* @private {Object}
351
*/
352
ChannelRequest.INVALID_CHUNK_ = {};
353
354
355
/**
356
* Sentinel value used to indicate an incomplete chunk in a multi-chunk
357
* response.
358
* @private {Object}
359
*/
360
ChannelRequest.INCOMPLETE_CHUNK_ = {};
361
362
363
/**
364
* Returns whether XHR streaming is supported on this browser.
365
*
366
* @return {boolean} Whether XHR streaming is supported.
367
* @see http://code.google.com/p/closure-library/issues/detail?id=346
368
*/
369
ChannelRequest.supportsXhrStreaming = function() {
370
return !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(10);
371
};
372
373
374
/**
375
* Sets extra HTTP headers to add to all the requests sent to the server.
376
*
377
* @param {Object} extraHeaders The HTTP headers.
378
*/
379
ChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {
380
this.extraHeaders_ = extraHeaders;
381
};
382
383
384
/**
385
* Sets the timeout for a request
386
*
387
* @param {number} timeout The timeout in MS for when we fail the request.
388
*/
389
ChannelRequest.prototype.setTimeout = function(timeout) {
390
this.timeout_ = timeout;
391
};
392
393
394
/**
395
* Sets the throttle for handling onreadystatechange events for the request.
396
*
397
* @param {number} throttle The throttle in ms. A value of zero indicates
398
* no throttle.
399
*/
400
ChannelRequest.prototype.setReadyStateChangeThrottle = function(throttle) {
401
this.readyStateChangeThrottleMs_ = throttle;
402
};
403
404
405
/**
406
* Uses XMLHTTP to send an HTTP POST to the server.
407
*
408
* @param {goog.Uri} uri The uri of the request.
409
* @param {string} postData The data for the post body.
410
* @param {boolean} decodeChunks Whether to the result is expected to be
411
* encoded for chunking and thus requires decoding.
412
*/
413
ChannelRequest.prototype.xmlHttpPost = function(uri, postData, decodeChunks) {
414
this.type_ = ChannelRequest.Type_.XML_HTTP;
415
this.baseUri_ = uri.clone().makeUnique();
416
this.postData_ = postData;
417
this.decodeChunks_ = decodeChunks;
418
this.sendXmlHttp_(null /* hostPrefix */);
419
};
420
421
422
/**
423
* Uses XMLHTTP to send an HTTP GET to the server.
424
*
425
* @param {goog.Uri} uri The uri of the request.
426
* @param {boolean} decodeChunks Whether to the result is expected to be
427
* encoded for chunking and thus requires decoding.
428
* @param {?string} hostPrefix The host prefix, if we might be using a
429
* secondary domain. Note that it should also be in the URL, adding this
430
* won't cause it to be added to the URL.
431
* @param {boolean=} opt_noClose Whether to request that the tcp/ip connection
432
* should be closed.
433
*/
434
ChannelRequest.prototype.xmlHttpGet = function(
435
uri, decodeChunks, hostPrefix, opt_noClose) {
436
this.type_ = ChannelRequest.Type_.XML_HTTP;
437
this.baseUri_ = uri.clone().makeUnique();
438
this.postData_ = null;
439
this.decodeChunks_ = decodeChunks;
440
if (opt_noClose) {
441
this.sendClose_ = false;
442
}
443
444
this.sendXmlHttp_(hostPrefix);
445
};
446
447
448
/**
449
* Sends a request via XMLHTTP according to the current state of the request
450
* object.
451
*
452
* @param {?string} hostPrefix The host prefix, if we might be using a secondary
453
* domain.
454
* @private
455
*/
456
ChannelRequest.prototype.sendXmlHttp_ = function(hostPrefix) {
457
this.requestStartTime_ = goog.now();
458
this.ensureWatchDogTimer_();
459
460
// clone the base URI to create the request URI. The request uri has the
461
// attempt number as a parameter which helps in debugging.
462
this.requestUri_ = this.baseUri_.clone();
463
this.requestUri_.setParameterValues('t', this.retryId_);
464
465
// send the request either as a POST or GET
466
this.xmlHttpChunkStart_ = 0;
467
var useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();
468
this.xmlHttp_ =
469
this.channel_.createXhrIo(useSecondaryDomains ? hostPrefix : null);
470
471
if (this.readyStateChangeThrottleMs_ > 0) {
472
this.readyStateChangeThrottle_ = new goog.async.Throttle(
473
goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),
474
this.readyStateChangeThrottleMs_);
475
}
476
477
this.eventHandler_.listen(
478
this.xmlHttp_, goog.net.EventType.READY_STATE_CHANGE,
479
this.readyStateChangeHandler_);
480
481
var headers = this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};
482
if (this.postData_) {
483
this.verb_ = 'POST';
484
headers['Content-Type'] = 'application/x-www-form-urlencoded';
485
this.xmlHttp_.send(this.requestUri_, this.verb_, this.postData_, headers);
486
} else {
487
this.verb_ = 'GET';
488
489
// If the user agent is webkit, we cannot send the close header since it is
490
// disallowed by the browser. If we attempt to set the "Connection: close"
491
// header in WEBKIT browser, it will actually causes an error message.
492
if (this.sendClose_ && !goog.userAgent.WEBKIT) {
493
headers['Connection'] = 'close';
494
}
495
this.xmlHttp_.send(this.requestUri_, this.verb_, null, headers);
496
}
497
requestStats.notifyServerReachabilityEvent(
498
requestStats.ServerReachability.REQUEST_MADE);
499
this.channelDebug_.xmlHttpChannelRequest(
500
this.verb_, this.requestUri_, this.rid_, this.retryId_, this.postData_);
501
};
502
503
504
/**
505
* Handles a readystatechange event.
506
* @param {goog.events.Event} evt The event.
507
* @private
508
*/
509
ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {
510
var xhr = /** @type {goog.net.XhrIo} */ (evt.target);
511
var throttle = this.readyStateChangeThrottle_;
512
if (throttle &&
513
xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
514
// Only throttle in the partial data case.
515
this.channelDebug_.debug('Throttling readystatechange.');
516
throttle.fire();
517
} else {
518
// If we haven't throttled, just handle response directly.
519
this.xmlHttpHandler_(xhr);
520
}
521
};
522
523
524
/**
525
* XmlHttp handler
526
* @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.
527
* @private
528
*/
529
ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {
530
requestStats.onStartExecution();
531
532
533
try {
534
if (xmlhttp == this.xmlHttp_) {
535
this.onXmlHttpReadyStateChanged_();
536
} else {
537
this.channelDebug_.warning(
538
'Called back with an ' +
539
'unexpected xmlhttp');
540
}
541
} catch (ex) {
542
this.channelDebug_.debug('Failed call to OnXmlHttpReadyStateChanged_');
543
if (this.xmlHttp_ && this.xmlHttp_.getResponseText()) {
544
this.channelDebug_.dumpException(
545
ex, 'ResponseText: ' + this.xmlHttp_.getResponseText());
546
} else {
547
this.channelDebug_.dumpException(ex, 'No response text');
548
}
549
} finally {
550
requestStats.onEndExecution();
551
}
552
};
553
554
555
/**
556
* Called by the readystate handler for XMLHTTP requests.
557
*
558
* @private
559
*/
560
ChannelRequest.prototype.onXmlHttpReadyStateChanged_ = function() {
561
var readyState = this.xmlHttp_.getReadyState();
562
var errorCode = this.xmlHttp_.getLastErrorCode();
563
var statusCode = this.xmlHttp_.getStatus();
564
565
// we get partial results in browsers that support ready state interactive.
566
// We also make sure that getResponseText is not null in interactive mode
567
// before we continue. However, we don't do it in Opera because it only
568
// fire readyState == INTERACTIVE once. We need the following code to poll
569
if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
570
readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&
571
!goog.userAgent.OPERA && !this.xmlHttp_.getResponseText()) {
572
// not yet ready
573
return;
574
}
575
576
// Dispatch any appropriate network events.
577
if (!this.cancelled_ && readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
578
errorCode != goog.net.ErrorCode.ABORT) {
579
// Pretty conservative, these are the only known scenarios which we'd
580
// consider indicative of a truly non-functional network connection.
581
if (errorCode == goog.net.ErrorCode.TIMEOUT || statusCode <= 0) {
582
requestStats.notifyServerReachabilityEvent(
583
requestStats.ServerReachability.REQUEST_FAILED);
584
} else {
585
requestStats.notifyServerReachabilityEvent(
586
requestStats.ServerReachability.REQUEST_SUCCEEDED);
587
}
588
}
589
590
// got some data so cancel the watchdog timer
591
this.cancelWatchDogTimer_();
592
593
var status = this.xmlHttp_.getStatus();
594
this.lastStatusCode_ = status;
595
var responseText = this.xmlHttp_.getResponseText();
596
if (!responseText) {
597
this.channelDebug_.debug(
598
'No response text for uri ' + this.requestUri_ + ' status ' + status);
599
}
600
this.successful_ = (status == 200);
601
602
this.channelDebug_.xmlHttpChannelResponseMetaData(
603
/** @type {string} */ (this.verb_), this.requestUri_, this.rid_,
604
this.retryId_, readyState, status);
605
606
if (!this.successful_) {
607
if (status == 400 && responseText.indexOf('Unknown SID') > 0) {
608
// the server error string will include 'Unknown SID' which indicates the
609
// server doesn't know about the session (maybe it got restarted, maybe
610
// the user got moved to another server, etc.,). Handlers can special
611
// case this error
612
this.lastError_ = ChannelRequest.Error.UNKNOWN_SESSION_ID;
613
requestStats.notifyStatEvent(
614
requestStats.Stat.REQUEST_UNKNOWN_SESSION_ID);
615
this.channelDebug_.warning('XMLHTTP Unknown SID (' + this.rid_ + ')');
616
} else {
617
this.lastError_ = ChannelRequest.Error.STATUS;
618
requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_STATUS);
619
this.channelDebug_.warning(
620
'XMLHTTP Bad status ' + status + ' (' + this.rid_ + ')');
621
}
622
this.cleanup_();
623
this.dispatchFailure_();
624
return;
625
}
626
627
if (this.decodeChunks_) {
628
this.decodeNextChunks_(readyState, responseText);
629
if (goog.userAgent.OPERA && this.successful_ &&
630
readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
631
this.startPolling_();
632
}
633
} else {
634
this.channelDebug_.xmlHttpChannelResponseText(
635
this.rid_, responseText, null);
636
this.safeOnRequestData_(responseText);
637
}
638
639
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
640
this.cleanup_();
641
}
642
643
if (!this.successful_) {
644
return;
645
}
646
647
if (!this.cancelled_) {
648
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
649
this.channel_.onRequestComplete(this);
650
} else {
651
// The default is false, the result from this callback shouldn't carry
652
// over to the next callback, otherwise the request looks successful if
653
// the watchdog timer gets called
654
this.successful_ = false;
655
this.ensureWatchDogTimer_();
656
}
657
}
658
};
659
660
661
/**
662
* Decodes the next set of available chunks in the response.
663
* @param {number} readyState The value of readyState.
664
* @param {string} responseText The value of responseText.
665
* @private
666
*/
667
ChannelRequest.prototype.decodeNextChunks_ = function(
668
readyState, responseText) {
669
var decodeNextChunksSuccessful = true;
670
while (!this.cancelled_ && this.xmlHttpChunkStart_ < responseText.length) {
671
var chunkText = this.getNextChunk_(responseText);
672
if (chunkText == ChannelRequest.INCOMPLETE_CHUNK_) {
673
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
674
// should have consumed entire response when the request is done
675
this.lastError_ = ChannelRequest.Error.BAD_DATA;
676
requestStats.notifyStatEvent(requestStats.Stat.REQUEST_INCOMPLETE_DATA);
677
decodeNextChunksSuccessful = false;
678
}
679
this.channelDebug_.xmlHttpChannelResponseText(
680
this.rid_, null, '[Incomplete Response]');
681
break;
682
} else if (chunkText == ChannelRequest.INVALID_CHUNK_) {
683
this.lastError_ = ChannelRequest.Error.BAD_DATA;
684
requestStats.notifyStatEvent(requestStats.Stat.REQUEST_BAD_DATA);
685
this.channelDebug_.xmlHttpChannelResponseText(
686
this.rid_, responseText, '[Invalid Chunk]');
687
decodeNextChunksSuccessful = false;
688
break;
689
} else {
690
this.channelDebug_.xmlHttpChannelResponseText(
691
this.rid_, /** @type {string} */ (chunkText), null);
692
this.safeOnRequestData_(/** @type {string} */ (chunkText));
693
}
694
}
695
if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
696
responseText.length == 0) {
697
// also an error if we didn't get any response
698
this.lastError_ = ChannelRequest.Error.NO_DATA;
699
requestStats.notifyStatEvent(requestStats.Stat.REQUEST_NO_DATA);
700
decodeNextChunksSuccessful = false;
701
}
702
this.successful_ = this.successful_ && decodeNextChunksSuccessful;
703
if (!decodeNextChunksSuccessful) {
704
// malformed response - we make this trigger retry logic
705
this.channelDebug_.xmlHttpChannelResponseText(
706
this.rid_, responseText, '[Invalid Chunked Response]');
707
this.cleanup_();
708
this.dispatchFailure_();
709
}
710
};
711
712
713
/**
714
* Polls the response for new data.
715
* @private
716
*/
717
ChannelRequest.prototype.pollResponse_ = function() {
718
var readyState = this.xmlHttp_.getReadyState();
719
var responseText = this.xmlHttp_.getResponseText();
720
if (this.xmlHttpChunkStart_ < responseText.length) {
721
this.cancelWatchDogTimer_();
722
this.decodeNextChunks_(readyState, responseText);
723
if (this.successful_ &&
724
readyState != goog.net.XmlHttp.ReadyState.COMPLETE) {
725
this.ensureWatchDogTimer_();
726
}
727
}
728
};
729
730
731
/**
732
* Starts a polling interval for changes to responseText of the
733
* XMLHttpRequest, for browsers that don't fire onreadystatechange
734
* as data comes in incrementally. This timer is disabled in
735
* cleanup_().
736
* @private
737
*/
738
ChannelRequest.prototype.startPolling_ = function() {
739
this.eventHandler_.listen(
740
this.pollingTimer_, goog.Timer.TICK, this.pollResponse_);
741
this.pollingTimer_.start();
742
};
743
744
745
/**
746
* Returns the next chunk of a chunk-encoded response. This is not standard
747
* HTTP chunked encoding because browsers don't expose the chunk boundaries to
748
* the application through XMLHTTP. So we have an additional chunk encoding at
749
* the application level that lets us tell where the beginning and end of
750
* individual responses are so that we can only try to eval a complete JS array.
751
*
752
* The encoding is the size of the chunk encoded as a decimal string followed
753
* by a newline followed by the data.
754
*
755
* @param {string} responseText The response text from the XMLHTTP response.
756
* @return {string|Object} The next chunk string or a sentinel object
757
* indicating a special condition.
758
* @private
759
*/
760
ChannelRequest.prototype.getNextChunk_ = function(responseText) {
761
var sizeStartIndex = this.xmlHttpChunkStart_;
762
var sizeEndIndex = responseText.indexOf('\n', sizeStartIndex);
763
if (sizeEndIndex == -1) {
764
return ChannelRequest.INCOMPLETE_CHUNK_;
765
}
766
767
var sizeAsString = responseText.substring(sizeStartIndex, sizeEndIndex);
768
var size = Number(sizeAsString);
769
if (isNaN(size)) {
770
return ChannelRequest.INVALID_CHUNK_;
771
}
772
773
var chunkStartIndex = sizeEndIndex + 1;
774
if (chunkStartIndex + size > responseText.length) {
775
return ChannelRequest.INCOMPLETE_CHUNK_;
776
}
777
778
var chunkText = responseText.substr(chunkStartIndex, size);
779
this.xmlHttpChunkStart_ = chunkStartIndex + size;
780
return chunkText;
781
};
782
783
784
/**
785
* Uses an IMG tag or navigator.sendBeacon to send an HTTP get to the server.
786
*
787
* This is only currently used to terminate the connection, as an IMG tag is
788
* the most reliable way to send something to the server while the page
789
* is getting torn down.
790
*
791
* Navigator.sendBeacon is available on Chrome and Firefox as a formal
792
* solution to ensure delivery without blocking window close. See
793
* https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
794
*
795
* For Chrome Apps, sendBeacon is always necessary due to Content Security
796
* Policy (CSP) violation of using an IMG tag.
797
*
798
* @param {goog.Uri} uri The uri to send a request to.
799
*/
800
ChannelRequest.prototype.sendCloseRequest = function(uri) {
801
this.type_ = ChannelRequest.Type_.CLOSE_REQUEST;
802
this.baseUri_ = uri.clone().makeUnique();
803
804
var requestSent = false;
805
806
if (goog.global.navigator && goog.global.navigator.sendBeacon) {
807
// empty string body to avoid 413 error on chrome < 41
808
requestSent =
809
goog.global.navigator.sendBeacon(this.baseUri_.toString(), '');
810
}
811
812
if (!requestSent) {
813
var eltImg = new Image();
814
eltImg.src = this.baseUri_;
815
}
816
817
this.requestStartTime_ = goog.now();
818
this.ensureWatchDogTimer_();
819
};
820
821
822
/**
823
* Cancels the request no matter what the underlying transport is.
824
*/
825
ChannelRequest.prototype.cancel = function() {
826
this.cancelled_ = true;
827
this.cleanup_();
828
};
829
830
831
/**
832
* Ensures that there is watchdog timeout which is used to ensure that
833
* the connection completes in time.
834
*
835
* @private
836
*/
837
ChannelRequest.prototype.ensureWatchDogTimer_ = function() {
838
this.watchDogTimeoutTime_ = goog.now() + this.timeout_;
839
this.startWatchDogTimer_(this.timeout_);
840
};
841
842
843
/**
844
* Starts the watchdog timer which is used to ensure that the connection
845
* completes in time.
846
* @param {number} time The number of milliseconds to wait.
847
* @private
848
*/
849
ChannelRequest.prototype.startWatchDogTimer_ = function(time) {
850
if (this.watchDogTimerId_ != null) {
851
// assertion
852
throw Error('WatchDog timer not null');
853
}
854
this.watchDogTimerId_ =
855
requestStats.setTimeout(goog.bind(this.onWatchDogTimeout_, this), time);
856
};
857
858
859
/**
860
* Cancels the watchdog timer if it has been started.
861
*
862
* @private
863
*/
864
ChannelRequest.prototype.cancelWatchDogTimer_ = function() {
865
if (this.watchDogTimerId_) {
866
goog.global.clearTimeout(this.watchDogTimerId_);
867
this.watchDogTimerId_ = null;
868
}
869
};
870
871
872
/**
873
* Called when the watchdog timer is triggered. It also handles a case where it
874
* is called too early which we suspect may be happening sometimes
875
* (not sure why)
876
*
877
* @private
878
*/
879
ChannelRequest.prototype.onWatchDogTimeout_ = function() {
880
this.watchDogTimerId_ = null;
881
var now = goog.now();
882
if (now - this.watchDogTimeoutTime_ >= 0) {
883
this.handleTimeout_();
884
} else {
885
// got called too early for some reason
886
this.channelDebug_.warning('WatchDog timer called too early');
887
this.startWatchDogTimer_(this.watchDogTimeoutTime_ - now);
888
}
889
};
890
891
892
/**
893
* Called when the request has actually timed out. Will cleanup and notify the
894
* channel of the failure.
895
*
896
* @private
897
*/
898
ChannelRequest.prototype.handleTimeout_ = function() {
899
if (this.successful_) {
900
// Should never happen.
901
this.channelDebug_.severe(
902
'Received watchdog timeout even though request loaded successfully');
903
}
904
905
this.channelDebug_.timeoutResponse(this.requestUri_);
906
907
// IMG or SendBeacon requests never notice if they were successful,
908
// and always 'time out'. This fact says nothing about reachability.
909
if (this.type_ != ChannelRequest.Type_.CLOSE_REQUEST) {
910
requestStats.notifyServerReachabilityEvent(
911
requestStats.ServerReachability.REQUEST_FAILED);
912
requestStats.notifyStatEvent(requestStats.Stat.REQUEST_TIMEOUT);
913
}
914
915
this.cleanup_();
916
917
// Set error and dispatch failure.
918
// This is called for CLOSE_REQUEST too to ensure channel_.onRequestComplete.
919
this.lastError_ = ChannelRequest.Error.TIMEOUT;
920
this.dispatchFailure_();
921
};
922
923
924
/**
925
* Notifies the channel that this request failed.
926
* @private
927
*/
928
ChannelRequest.prototype.dispatchFailure_ = function() {
929
if (this.channel_.isClosed() || this.cancelled_) {
930
return;
931
}
932
933
this.channel_.onRequestComplete(this);
934
};
935
936
937
/**
938
* Cleans up the objects used to make the request. This function is
939
* idempotent.
940
*
941
* @private
942
*/
943
ChannelRequest.prototype.cleanup_ = function() {
944
this.cancelWatchDogTimer_();
945
946
goog.dispose(this.readyStateChangeThrottle_);
947
this.readyStateChangeThrottle_ = null;
948
949
// Stop the polling timer, if necessary.
950
this.pollingTimer_.stop();
951
952
// Unhook all event handlers.
953
this.eventHandler_.removeAll();
954
955
if (this.xmlHttp_) {
956
// clear out this.xmlHttp_ before aborting so we handle getting reentered
957
// inside abort
958
var xmlhttp = this.xmlHttp_;
959
this.xmlHttp_ = null;
960
xmlhttp.abort();
961
xmlhttp.dispose();
962
}
963
};
964
965
966
/**
967
* Indicates whether the request was successful. Only valid after the handler
968
* is called to indicate completion of the request.
969
*
970
* @return {boolean} True if the request succeeded.
971
*/
972
ChannelRequest.prototype.getSuccess = function() {
973
return this.successful_;
974
};
975
976
977
/**
978
* If the request was not successful, returns the reason.
979
*
980
* @return {?ChannelRequest.Error} The last error.
981
*/
982
ChannelRequest.prototype.getLastError = function() {
983
return this.lastError_;
984
};
985
986
987
/**
988
* Returns the status code of the last request.
989
* @return {number} The status code of the last request.
990
*/
991
ChannelRequest.prototype.getLastStatusCode = function() {
992
return this.lastStatusCode_;
993
};
994
995
996
/**
997
* Returns the session id for this channel.
998
*
999
* @return {string|undefined} The session ID.
1000
*/
1001
ChannelRequest.prototype.getSessionId = function() {
1002
return this.sid_;
1003
};
1004
1005
1006
/**
1007
* Returns the request id for this request. Each request has a unique request
1008
* id and the request IDs are a sequential increasing count.
1009
*
1010
* @return {string|number|undefined} The request ID.
1011
*/
1012
ChannelRequest.prototype.getRequestId = function() {
1013
return this.rid_;
1014
};
1015
1016
1017
/**
1018
* Returns the data for a post, if this request is a post.
1019
*
1020
* @return {?string} The POST data provided by the request initiator.
1021
*/
1022
ChannelRequest.prototype.getPostData = function() {
1023
return this.postData_;
1024
};
1025
1026
1027
/**
1028
* Returns the XhrIo request object.
1029
*
1030
* @return {?goog.net.XhrIo} Any XhrIo request created for this object.
1031
*/
1032
ChannelRequest.prototype.getXhr = function() {
1033
return this.xmlHttp_;
1034
};
1035
1036
1037
/**
1038
* Returns the time that the request started, if it has started.
1039
*
1040
* @return {?number} The time the request started, as returned by goog.now().
1041
*/
1042
ChannelRequest.prototype.getRequestStartTime = function() {
1043
return this.requestStartTime_;
1044
};
1045
1046
1047
/**
1048
* Helper to call the callback's onRequestData, which catches any
1049
* exception and cleans up the request.
1050
* @param {string} data The request data.
1051
* @private
1052
*/
1053
ChannelRequest.prototype.safeOnRequestData_ = function(data) {
1054
1055
try {
1056
this.channel_.onRequestData(this, data);
1057
var stats = requestStats.ServerReachability;
1058
requestStats.notifyServerReachabilityEvent(stats.BACK_CHANNEL_ACTIVITY);
1059
} catch (e) {
1060
// Dump debug info, but keep going without closing the channel.
1061
this.channelDebug_.dumpException(e, 'Error in httprequest callback');
1062
}
1063
};
1064
1065
1066
/**
1067
* Convenience factory method.
1068
*
1069
* @param {Channel} channel The channel object that owns this request.
1070
* @param {WebChannelDebug} channelDebug A WebChannelDebug to use for logging.
1071
* @param {string=} opt_sessionId The session id for the channel.
1072
* @param {string|number=} opt_requestId The request id for this request.
1073
* @param {number=} opt_retryId The retry id for this request.
1074
* @return {!ChannelRequest} The created channel request.
1075
*/
1076
ChannelRequest.createChannelRequest = function(
1077
channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
1078
return new ChannelRequest(
1079
channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId);
1080
};
1081
}); // goog.scope
1082
1083