Skip to content

Commit d347536

Browse files
committed
Merge remote-tracking branch 'origin/master' into gh-pages
* origin/master: v3.2.0 Response timeout (#1128) goto fail syntax Complain when .end() is called more than once Split .pipe() to avoid calling .end() second time after redirect btoa no longer required Fire timeout error Unify timeout error Moved test Moved Use common test server Fixed should no-op Use promises to test Catch errors in the .end() callback Use common server for basic auth Use btoa by default only if it's available (#1123) Fixed undefined query when sorting without query strings (#1124)
2 parents 1dd8191 + 6365669 commit d347536

22 files changed

+927
-453
lines changed

History.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# 3.2.0 (2016-12-11)
2+
3+
* Added `.timeout({response:ms})`, which allows limiting maximum response time independently from total download time (Kornel Lesiński)
4+
* Added warnings when `.end()` is called more than once (Kornel Lesiński)
5+
* Added `response.links` to browser version (Lukas Eipert)
6+
* `btoa` is no longer required in IE9 (Kornel Lesiński)
7+
* Fixed `.sortQuery()` on URLs without query strings (Kornel Lesiński)
8+
* Refactored common response code into `ResponseBase` (Lukas Eipert)
9+
110
# 3.1.0 (2016-11-28)
211

312
* Added `.sortQuery()` (vicanso)

Readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Tested browsers:
3535
- Latest Android, iPhone
3636
- IE10 through latest. IE9 with polyfills.
3737

38-
Even though IE9 is supported, a polyfill for `window.FormData` is required for `.field()`, and `window.btoa` is needed to use basic auth.
38+
Even though IE9 is supported, a polyfill for `window.FormData` is required for `.field()`.
3939

4040
Node 4 or later is required.
4141

docs/index.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,27 @@ For more information, see the Mozilla Developer Network [xhr.responseType docs](
353353

354354
To abort requests simply invoke the `req.abort()` method.
355355

356-
## Request timeouts
356+
## Timeouts
357357

358-
A timeout can be applied by invoking `req.timeout(ms)`, after which an error
359-
will be triggered. To differentiate between other errors the `err.timeout` property
360-
is set to the `ms` value. __NOTE__ that this is a timeout applied to the request
361-
and all subsequent redirects, not per request.
358+
Sometimes networks and servers get "stuck" and never respond after accepting a request. Set timeouts to avoid requests waiting forever.
359+
360+
* `req.timeout({deadline:ms})` or `req.timeout(ms)` (where `ms` is a number of milliseconds > 0) sets a deadline for the entire request (including all redirects) to complete. If the response isn't fully downloaded within that time, the request will be aborted.
361+
362+
* `req.timeout({response:ms})` sets maximum time to wait for the first byte to arrive from the server, but it does not limit how long the entire download can take. Response timeout should be a few seconds longer than just the time it takes server to respond, because it also includes time to make DNS lookup, TCP/IP and TLS connections.
363+
364+
You should use both `deadline` and `response` timeouts. This way you can use a short response timeout to detect unresponsive networks quickly, and a long deadline to give time for downloads on slow, but reliable, networks.
365+
366+
request
367+
.get('/big-file?network=slow')
368+
.timeout({
369+
response: 5000, // Wait 5 seconds for the server to start sending,
370+
deadline: 60000, // but allow 1 minute for the file to finish loading.
371+
})
372+
.end(function(err, res){
373+
if (err.timeout) { /* timed out! */ }
374+
});
375+
376+
Timeout errors have a `.timeout` property.
362377

363378
## Authentication
364379

index.html

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,11 +265,25 @@ <h3 id="response-status">Response status</h3>
265265
res.forbidden = 403 == status;
266266
</code></pre><h2 id="aborting-requests">Aborting requests</h2>
267267
<p> To abort requests simply invoke the <code>req.abort()</code> method.</p>
268-
<h2 id="request-timeouts">Request timeouts</h2>
269-
<p> A timeout can be applied by invoking <code>req.timeout(ms)</code>, after which an error
270-
will be triggered. To differentiate between other errors the <code>err.timeout</code> property
271-
is set to the <code>ms</code> value. <strong>NOTE</strong> that this is a timeout applied to the request
272-
and all subsequent redirects, not per request.</p>
268+
<h2 id="timeouts">Timeouts</h2>
269+
<p>Sometimes networks and servers get &quot;stuck&quot; and never respond after accepting a request. Set timeouts to avoid requests waiting forever.</p>
270+
<ul>
271+
<li><p><code>req.timeout({deadline:ms})</code> or <code>req.timeout(ms)</code> (where <code>ms</code> is a number of milliseconds &gt; 0) sets a deadline for the entire request (including all redirects) to complete. If the response isn&#39;t fully downloaded within that time, the request will be aborted.</p>
272+
</li>
273+
<li><p><code>req.timeout({response:ms})</code> sets maximum time to wait for the first byte to arrive from the server, but it does not limit how long the entire download can take. Response timeout should be a few seconds longer than just the time it takes server to respond, because it also includes time to make DNS lookup, TCP/IP and TLS connections.</p>
274+
</li>
275+
</ul>
276+
<p>You should use both <code>deadline</code> and <code>response</code> timeouts. This way you can use a short response timeout to detect unresponsive networks quickly, and a long deadline to give time for downloads on slow, but reliable, networks.</p>
277+
<pre><code>request
278+
.get(&#39;/big-file?network=slow&#39;)
279+
.timeout({
280+
response: 5000, // Wait 5 seconds for the server to start sending,
281+
deadline: 60000, // but allow 1 minute for the file to finish loading.
282+
})
283+
.end(function(err, res){
284+
if (err.timeout) { /* timed out! */ }
285+
});
286+
</code></pre><p>Timeout errors have a <code>.timeout</code> property.</p>
273287
<h2 id="authentication">Authentication</h2>
274288
<p> In both Node and browsers auth available via the <code>.auth()</code> method:</p>
275289
<pre><code>request

lib/client.js

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -521,14 +521,13 @@ Request.prototype.accept = function(type){
521521
Request.prototype.auth = function(user, pass, options){
522522
if (!options) {
523523
options = {
524-
type: 'basic'
524+
type: 'function' === typeof btoa ? 'basic' : 'auto',
525525
}
526526
}
527527

528528
switch (options.type) {
529529
case 'basic':
530-
var str = btoa(user + ':' + pass);
531-
this.set('Authorization', 'Basic ' + str);
530+
this.set('Authorization', 'Basic ' + btoa(user + ':' + pass));
532531
break;
533532

534533
case 'auto':
@@ -640,19 +639,6 @@ Request.prototype.pipe = Request.prototype.write = function(){
640639
throw Error("Streaming is not supported in browser version of superagent");
641640
};
642641

643-
/**
644-
* Invoke callback with timeout error.
645-
*
646-
* @api private
647-
*/
648-
649-
Request.prototype._timeoutError = function(){
650-
var timeout = this._timeout;
651-
var err = new Error('timeout of ' + timeout + 'ms exceeded');
652-
err.timeout = timeout;
653-
this.callback(err);
654-
};
655-
656642
/**
657643
* Compose querystring to append to req.url
658644
*
@@ -662,9 +648,20 @@ Request.prototype._timeoutError = function(){
662648
Request.prototype._appendQueryString = function(){
663649
var query = this._query.join('&');
664650
if (query) {
665-
this.url += ~this.url.indexOf('?')
666-
? '&' + query
667-
: '?' + query;
651+
this.url += (this.url.indexOf('?') >= 0 ? '&' : '?') + query;
652+
}
653+
654+
if (this._sort) {
655+
var index = this.url.indexOf('?');
656+
if (index >= 0) {
657+
var queryArr = this.url.substring(index + 1).split('&');
658+
if (isFunction(this._sort)) {
659+
queryArr.sort(this._sort);
660+
} else {
661+
queryArr.sort();
662+
}
663+
this.url = this.url.substring(0, index) + '?' + queryArr.join('&');
664+
}
668665
}
669666
};
670667

@@ -693,24 +690,33 @@ Request.prototype._isHost = function _isHost(obj) {
693690
Request.prototype.end = function(fn){
694691
var self = this;
695692
var xhr = this.xhr = request.getXHR();
696-
var timeout = this._timeout;
697693
var data = this._formData || this._data;
698694

695+
if (this._endCalled) {
696+
console.warn("Warning: .end() was called twice. This is not supported in superagent");
697+
}
698+
this._endCalled = true;
699+
699700
// store callback
700701
this._callback = fn || noop;
701702

702703
// state change
703704
xhr.onreadystatechange = function(){
704-
if (4 != xhr.readyState) return;
705+
var readyState = xhr.readyState;
706+
if (readyState >= 2 && self._responseTimeoutTimer) {
707+
clearTimeout(self._responseTimeoutTimer);
708+
}
709+
if (4 != readyState) {
710+
return;
711+
}
705712

706713
// In IE9, reads to any property (e.g. status) off of an aborted XHR will
707714
// result in the error "Could not complete the operation due to error c00c023f"
708715
var status;
709716
try { status = xhr.status } catch(e) { status = 0; }
710717

711-
if (0 == status) {
712-
if (self.timedout) return self._timeoutError();
713-
if (self._aborted) return;
718+
if (!status) {
719+
if (self.timedout || self._aborted) return;
714720
return self.crossDomainError();
715721
}
716722
self.emit('end');
@@ -737,25 +743,10 @@ Request.prototype.end = function(fn){
737743
}
738744
}
739745

740-
// timeout
741-
if (timeout && !this._timer) {
742-
this._timer = setTimeout(function(){
743-
self.timedout = true;
744-
self.abort();
745-
}, timeout);
746-
}
747-
748746
// querystring
749747
this._appendQueryString();
750748

751-
if (this._sort) {
752-
var index = this.url.indexOf('?');
753-
if (~index) {
754-
var queryArr = this.url.substring(index + 1).split('&');
755-
isFunction(this._sort) ? queryArr.sort(this._sort) : queryArr.sort();
756-
}
757-
this.url = this.url.substring(0, index) + '?' + queryArr.join('&');
758-
}
749+
this._setTimeouts();
759750

760751
// initiate request
761752
if (this.username && this.password) {
@@ -772,7 +763,9 @@ Request.prototype.end = function(fn){
772763
// serialize stuff
773764
var contentType = this._header['content-type'];
774765
var serialize = this._serializer || request.serialize[contentType ? contentType.split(';')[0] : ''];
775-
if (!serialize && isJSON(contentType)) serialize = request.serialize['application/json'];
766+
if (!serialize && isJSON(contentType)) {
767+
serialize = request.serialize['application/json'];
768+
}
776769
if (serialize) data = serialize(data);
777770
}
778771

lib/node/index.js

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,17 @@ Request.prototype.write = function(data, encoding){
345345
Request.prototype.pipe = function(stream, options){
346346
this.piped = true; // HACK...
347347
this.buffer(false);
348+
this.end();
349+
return this._pipeContinue(stream, options);
350+
};
351+
352+
Request.prototype._pipeContinue = function(stream, options){
348353
var self = this;
349-
this.end().req.once('response', function(res){
354+
this.req.once('response', function(res){
350355
// redirect
351356
var redirect = isRedirect(res.statusCode);
352357
if (redirect && self._redirects++ != self._maxRedirects) {
353-
return self._redirect(res).pipe(stream, options);
358+
return self._redirect(res)._pipeContinue(stream, options);
354359
}
355360

356361
self.res = res;
@@ -446,6 +451,7 @@ Request.prototype._redirect = function(res){
446451
_initHeaders(this)
447452

448453
// redirect
454+
this._endCalled = false;
449455
this.url = url;
450456
this.qs = {};
451457
this.qsRaw = [];
@@ -575,7 +581,9 @@ Request.prototype.request = function(){
575581

576582
// request
577583
var req = this.req = mod.request(options);
578-
if ('HEAD' != options.method) req.setHeader('Accept-Encoding', 'gzip, deflate');
584+
if ('HEAD' != options.method) {
585+
req.setHeader('Accept-Encoding', 'gzip, deflate');
586+
}
579587
this.protocol = url.protocol;
580588
this.host = url.host;
581589

@@ -661,18 +669,21 @@ Request.prototype.callback = function(err, res){
661669
*/
662670

663671
Request.prototype._appendQueryString = function(req){
664-
var querystring = qs.stringify(this.qs, { indices: false, strictNullHandling: true });
665-
querystring += ((querystring.length && this.qsRaw.length) ? '&' : '') + this.qsRaw.join('&');
666-
req.path += querystring.length
667-
? (~req.path.indexOf('?') ? '&' : '?') + querystring
668-
: '';
672+
var query = qs.stringify(this.qs, { indices: false, strictNullHandling: true });
673+
query += ((query.length && this.qsRaw.length) ? '&' : '') + this.qsRaw.join('&');
674+
req.path += query.length ? (~req.path.indexOf('?') ? '&' : '?') + query : '';
675+
669676
if (this._sort) {
670677
var index = req.path.indexOf('?');
671-
if (~index) {
678+
if (index >= 0) {
672679
var queryArr = req.path.substring(index + 1).split('&');
673-
isFunction(this._sort) ? queryArr.sort(this._sort) : queryArr.sort();
680+
if (isFunction(this._sort)) {
681+
queryArr.sort(this._sort);
682+
} else {
683+
queryArr.sort();
684+
}
685+
req.path = req.path.substring(0, index) + '?' + queryArr.join('&');
674686
}
675-
req.path = req.path.substring(0, index) + '?' + queryArr.join('&');
676687
}
677688
};
678689

@@ -714,9 +725,13 @@ Request.prototype.end = function(fn){
714725
var req = this.request();
715726
var buffer = this._buffer;
716727
var method = this.method;
717-
var timeout = this._timeout;
718728
debug('%s %s', this.method, this.url);
719729

730+
if (this._endCalled) {
731+
console.warn("Warning: .end() was called twice. This is not supported in superagent");
732+
}
733+
this._endCalled = true;
734+
720735
// store callback
721736
this._callback = fn || noop;
722737

@@ -727,18 +742,7 @@ Request.prototype.end = function(fn){
727742
return this.callback(e);
728743
}
729744

730-
// timeout
731-
if (timeout && !this._timer) {
732-
debug('timeout %sms %s %s', timeout, this.method, this.url);
733-
this._timer = setTimeout(function(){
734-
var err = new Error('timeout of ' + timeout + 'ms exceeded');
735-
err.timeout = timeout;
736-
err.code = 'ECONNABORTED';
737-
self.timedout = true;
738-
self.abort();
739-
self.callback(err);
740-
}, timeout);
741-
}
745+
this._setTimeouts();
742746

743747
// body
744748
if ('HEAD' != method && !req._headerSent) {
@@ -748,7 +752,9 @@ Request.prototype.end = function(fn){
748752
// Parse out just the content type from the header (ignore the charset)
749753
if (contentType) contentType = contentType.split(';')[0]
750754
var serialize = exports.serialize[contentType];
751-
if (!serialize && isJSON(contentType)) serialize = exports.serialize['application/json'];
755+
if (!serialize && isJSON(contentType)) {
756+
serialize = exports.serialize['application/json'];
757+
}
752758
if (serialize) data = serialize(data);
753759
}
754760

@@ -762,6 +768,10 @@ Request.prototype.end = function(fn){
762768
req.once('response', function(res){
763769
debug('%s %s -> %s', self.method, self.url, res.statusCode);
764770

771+
if (self._responseTimeoutTimer) {
772+
clearTimeout(self._responseTimeoutTimer);
773+
}
774+
765775
if (self.piped) {
766776
return;
767777
}
@@ -815,7 +825,9 @@ Request.prototype.end = function(fn){
815825
}
816826

817827
// by default only buffer text/*, json and messed up thing from hell
818-
if (undefined === buffer && isText(mime) || isJSON(mime)) buffer = true;
828+
if (undefined === buffer && isText(mime) || isJSON(mime)) {
829+
buffer = true;
830+
}
819831

820832
var parserHandlesEnd = false;
821833
if (parser) {

0 commit comments

Comments
 (0)