UNPKG

22 kBJavaScriptView Raw
1/**
2 * Root reference for iframes.
3 */
4
5var root;
6if (typeof window !== 'undefined') { // Browser window
7 root = window;
8} else if (typeof self !== 'undefined') { // Web Worker
9 root = self;
10} else { // Other environments
11 console.warn("Using browser-only version of superagent in non-browser environment");
12 root = this;
13}
14
15var Emitter = require('component-emitter');
16var RequestBase = require('./request-base');
17var isObject = require('./is-object');
18var isFunction = require('./is-function');
19var ResponseBase = require('./response-base');
20var shouldRetry = require('./should-retry');
21
22/**
23 * Noop.
24 */
25
26function noop(){};
27
28/**
29 * Expose `request`.
30 */
31
32var request = exports = module.exports = function(method, url) {
33 // callback
34 if ('function' == typeof url) {
35 return new exports.Request('GET', method).end(url);
36 }
37
38 // url first
39 if (1 == arguments.length) {
40 return new exports.Request('GET', method);
41 }
42
43 return new exports.Request(method, url);
44}
45
46exports.Request = Request;
47
48/**
49 * Determine XHR.
50 */
51
52request.getXHR = function () {
53 if (root.XMLHttpRequest
54 && (!root.location || 'file:' != root.location.protocol
55 || !root.ActiveXObject)) {
56 return new XMLHttpRequest;
57 } else {
58 try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch(e) {}
59 try { return new ActiveXObject('Msxml2.XMLHTTP.6.0'); } catch(e) {}
60 try { return new ActiveXObject('Msxml2.XMLHTTP.3.0'); } catch(e) {}
61 try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch(e) {}
62 }
63 throw Error("Browser-only verison of superagent could not find XHR");
64};
65
66/**
67 * Removes leading and trailing whitespace, added to support IE.
68 *
69 * @param {String} s
70 * @return {String}
71 * @api private
72 */
73
74var trim = ''.trim
75 ? function(s) { return s.trim(); }
76 : function(s) { return s.replace(/(^\s*|\s*$)/g, ''); };
77
78/**
79 * Serialize the given `obj`.
80 *
81 * @param {Object} obj
82 * @return {String}
83 * @api private
84 */
85
86function serialize(obj) {
87 if (!isObject(obj)) return obj;
88 var pairs = [];
89 for (var key in obj) {
90 pushEncodedKeyValuePair(pairs, key, obj[key]);
91 }
92 return pairs.join('&');
93}
94
95/**
96 * Helps 'serialize' with serializing arrays.
97 * Mutates the pairs array.
98 *
99 * @param {Array} pairs
100 * @param {String} key
101 * @param {Mixed} val
102 */
103
104function pushEncodedKeyValuePair(pairs, key, val) {
105 if (val != null) {
106 if (Array.isArray(val)) {
107 val.forEach(function(v) {
108 pushEncodedKeyValuePair(pairs, key, v);
109 });
110 } else if (isObject(val)) {
111 for(var subkey in val) {
112 pushEncodedKeyValuePair(pairs, key + '[' + subkey + ']', val[subkey]);
113 }
114 } else {
115 pairs.push(encodeURIComponent(key)
116 + '=' + encodeURIComponent(val));
117 }
118 } else if (val === null) {
119 pairs.push(encodeURIComponent(key));
120 }
121}
122
123/**
124 * Expose serialization method.
125 */
126
127 request.serializeObject = serialize;
128
129 /**
130 * Parse the given x-www-form-urlencoded `str`.
131 *
132 * @param {String} str
133 * @return {Object}
134 * @api private
135 */
136
137function parseString(str) {
138 var obj = {};
139 var pairs = str.split('&');
140 var pair;
141 var pos;
142
143 for (var i = 0, len = pairs.length; i < len; ++i) {
144 pair = pairs[i];
145 pos = pair.indexOf('=');
146 if (pos == -1) {
147 obj[decodeURIComponent(pair)] = '';
148 } else {
149 obj[decodeURIComponent(pair.slice(0, pos))] =
150 decodeURIComponent(pair.slice(pos + 1));
151 }
152 }
153
154 return obj;
155}
156
157/**
158 * Expose parser.
159 */
160
161request.parseString = parseString;
162
163/**
164 * Default MIME type map.
165 *
166 * superagent.types.xml = 'application/xml';
167 *
168 */
169
170request.types = {
171 html: 'text/html',
172 json: 'application/json',
173 xml: 'application/xml',
174 urlencoded: 'application/x-www-form-urlencoded',
175 'form': 'application/x-www-form-urlencoded',
176 'form-data': 'application/x-www-form-urlencoded'
177};
178
179/**
180 * Default serialization map.
181 *
182 * superagent.serialize['application/xml'] = function(obj){
183 * return 'generated xml here';
184 * };
185 *
186 */
187
188 request.serialize = {
189 'application/x-www-form-urlencoded': serialize,
190 'application/json': JSON.stringify
191 };
192
193 /**
194 * Default parsers.
195 *
196 * superagent.parse['application/xml'] = function(str){
197 * return { object parsed from str };
198 * };
199 *
200 */
201
202request.parse = {
203 'application/x-www-form-urlencoded': parseString,
204 'application/json': JSON.parse
205};
206
207/**
208 * Parse the given header `str` into
209 * an object containing the mapped fields.
210 *
211 * @param {String} str
212 * @return {Object}
213 * @api private
214 */
215
216function parseHeader(str) {
217 var lines = str.split(/\r?\n/);
218 var fields = {};
219 var index;
220 var line;
221 var field;
222 var val;
223
224 lines.pop(); // trailing CRLF
225
226 for (var i = 0, len = lines.length; i < len; ++i) {
227 line = lines[i];
228 index = line.indexOf(':');
229 field = line.slice(0, index).toLowerCase();
230 val = trim(line.slice(index + 1));
231 fields[field] = val;
232 }
233
234 return fields;
235}
236
237/**
238 * Check if `mime` is json or has +json structured syntax suffix.
239 *
240 * @param {String} mime
241 * @return {Boolean}
242 * @api private
243 */
244
245function isJSON(mime) {
246 return /[\/+]json\b/.test(mime);
247}
248
249/**
250 * Initialize a new `Response` with the given `xhr`.
251 *
252 * - set flags (.ok, .error, etc)
253 * - parse header
254 *
255 * Examples:
256 *
257 * Aliasing `superagent` as `request` is nice:
258 *
259 * request = superagent;
260 *
261 * We can use the promise-like API, or pass callbacks:
262 *
263 * request.get('/').end(function(res){});
264 * request.get('/', function(res){});
265 *
266 * Sending data can be chained:
267 *
268 * request
269 * .post('/user')
270 * .send({ name: 'tj' })
271 * .end(function(res){});
272 *
273 * Or passed to `.send()`:
274 *
275 * request
276 * .post('/user')
277 * .send({ name: 'tj' }, function(res){});
278 *
279 * Or passed to `.post()`:
280 *
281 * request
282 * .post('/user', { name: 'tj' })
283 * .end(function(res){});
284 *
285 * Or further reduced to a single call for simple cases:
286 *
287 * request
288 * .post('/user', { name: 'tj' }, function(res){});
289 *
290 * @param {XMLHTTPRequest} xhr
291 * @param {Object} options
292 * @api private
293 */
294
295function Response(req) {
296 this.req = req;
297 this.xhr = this.req.xhr;
298 // responseText is accessible only if responseType is '' or 'text' and on older browsers
299 this.text = ((this.req.method !='HEAD' && (this.xhr.responseType === '' || this.xhr.responseType === 'text')) || typeof this.xhr.responseType === 'undefined')
300 ? this.xhr.responseText
301 : null;
302 this.statusText = this.req.xhr.statusText;
303 var status = this.xhr.status;
304 // handle IE9 bug: http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
305 if (status === 1223) {
306 status = 204;
307 }
308 this._setStatusProperties(status);
309 this.header = this.headers = parseHeader(this.xhr.getAllResponseHeaders());
310 // getAllResponseHeaders sometimes falsely returns "" for CORS requests, but
311 // getResponseHeader still works. so we get content-type even if getting
312 // other headers fails.
313 this.header['content-type'] = this.xhr.getResponseHeader('content-type');
314 this._setHeaderProperties(this.header);
315
316 if (null === this.text && req._responseType) {
317 this.body = this.xhr.response;
318 } else {
319 this.body = this.req.method != 'HEAD'
320 ? this._parseBody(this.text ? this.text : this.xhr.response)
321 : null;
322 }
323}
324
325ResponseBase(Response.prototype);
326
327/**
328 * Parse the given body `str`.
329 *
330 * Used for auto-parsing of bodies. Parsers
331 * are defined on the `superagent.parse` object.
332 *
333 * @param {String} str
334 * @return {Mixed}
335 * @api private
336 */
337
338Response.prototype._parseBody = function(str){
339 var parse = request.parse[this.type];
340 if(this.req._parser) {
341 return this.req._parser(this, str);
342 }
343 if (!parse && isJSON(this.type)) {
344 parse = request.parse['application/json'];
345 }
346 return parse && str && (str.length || str instanceof Object)
347 ? parse(str)
348 : null;
349};
350
351/**
352 * Return an `Error` representative of this response.
353 *
354 * @return {Error}
355 * @api public
356 */
357
358Response.prototype.toError = function(){
359 var req = this.req;
360 var method = req.method;
361 var url = req.url;
362
363 var msg = 'cannot ' + method + ' ' + url + ' (' + this.status + ')';
364 var err = new Error(msg);
365 err.status = this.status;
366 err.method = method;
367 err.url = url;
368
369 return err;
370};
371
372/**
373 * Expose `Response`.
374 */
375
376request.Response = Response;
377
378/**
379 * Initialize a new `Request` with the given `method` and `url`.
380 *
381 * @param {String} method
382 * @param {String} url
383 * @api public
384 */
385
386function Request(method, url) {
387 var self = this;
388 this._query = this._query || [];
389 this.method = method;
390 this.url = url;
391 this.header = {}; // preserves header name case
392 this._header = {}; // coerces header names to lowercase
393 this.on('end', function(){
394 var err = null;
395 var res = null;
396
397 try {
398 res = new Response(self);
399 } catch(e) {
400 err = new Error('Parser is unable to parse the response');
401 err.parse = true;
402 err.original = e;
403 // issue #675: return the raw response if the response parsing fails
404 if (self.xhr) {
405 // ie9 doesn't have 'response' property
406 err.rawResponse = typeof self.xhr.responseType == 'undefined' ? self.xhr.responseText : self.xhr.response;
407 // issue #876: return the http status code if the response parsing fails
408 err.status = self.xhr.status ? self.xhr.status : null;
409 err.statusCode = err.status; // backwards-compat only
410 } else {
411 err.rawResponse = null;
412 err.status = null;
413 }
414
415 return self.callback(err);
416 }
417
418 self.emit('response', res);
419
420 var new_err;
421 try {
422 if (!self._isResponseOK(res)) {
423 new_err = new Error(res.statusText || 'Unsuccessful HTTP response');
424 new_err.original = err;
425 new_err.response = res;
426 new_err.status = res.status;
427 }
428 } catch(e) {
429 new_err = e; // #985 touching res may cause INVALID_STATE_ERR on old Android
430 }
431
432 // #1000 don't catch errors from the callback to avoid double calling it
433 if (new_err) {
434 self.callback(new_err, res);
435 } else {
436 self.callback(null, res);
437 }
438 });
439}
440
441/**
442 * Mixin `Emitter` and `RequestBase`.
443 */
444
445Emitter(Request.prototype);
446RequestBase(Request.prototype);
447
448/**
449 * Set Content-Type to `type`, mapping values from `request.types`.
450 *
451 * Examples:
452 *
453 * superagent.types.xml = 'application/xml';
454 *
455 * request.post('/')
456 * .type('xml')
457 * .send(xmlstring)
458 * .end(callback);
459 *
460 * request.post('/')
461 * .type('application/xml')
462 * .send(xmlstring)
463 * .end(callback);
464 *
465 * @param {String} type
466 * @return {Request} for chaining
467 * @api public
468 */
469
470Request.prototype.type = function(type){
471 this.set('Content-Type', request.types[type] || type);
472 return this;
473};
474
475/**
476 * Set Accept to `type`, mapping values from `request.types`.
477 *
478 * Examples:
479 *
480 * superagent.types.json = 'application/json';
481 *
482 * request.get('/agent')
483 * .accept('json')
484 * .end(callback);
485 *
486 * request.get('/agent')
487 * .accept('application/json')
488 * .end(callback);
489 *
490 * @param {String} accept
491 * @return {Request} for chaining
492 * @api public
493 */
494
495Request.prototype.accept = function(type){
496 this.set('Accept', request.types[type] || type);
497 return this;
498};
499
500/**
501 * Set Authorization field value with `user` and `pass`.
502 *
503 * @param {String} user
504 * @param {String} [pass] optional in case of using 'bearer' as type
505 * @param {Object} options with 'type' property 'auto', 'basic' or 'bearer' (default 'basic')
506 * @return {Request} for chaining
507 * @api public
508 */
509
510Request.prototype.auth = function(user, pass, options){
511 if (typeof pass === 'object' && pass !== null) { // pass is optional and can substitute for options
512 options = pass;
513 }
514 if (!options) {
515 options = {
516 type: 'function' === typeof btoa ? 'basic' : 'auto',
517 }
518 }
519
520 switch (options.type) {
521 case 'basic':
522 this.set('Authorization', 'Basic ' + btoa(user + ':' + pass));
523 break;
524
525 case 'auto':
526 this.username = user;
527 this.password = pass;
528 break;
529
530 case 'bearer': // usage would be .auth(accessToken, { type: 'bearer' })
531 this.set('Authorization', 'Bearer ' + user);
532 break;
533 }
534 return this;
535};
536
537/**
538 * Add query-string `val`.
539 *
540 * Examples:
541 *
542 * request.get('/shoes')
543 * .query('size=10')
544 * .query({ color: 'blue' })
545 *
546 * @param {Object|String} val
547 * @return {Request} for chaining
548 * @api public
549 */
550
551Request.prototype.query = function(val){
552 if ('string' != typeof val) val = serialize(val);
553 if (val) this._query.push(val);
554 return this;
555};
556
557/**
558 * Queue the given `file` as an attachment to the specified `field`,
559 * with optional `options` (or filename).
560 *
561 * ``` js
562 * request.post('/upload')
563 * .attach('content', new Blob(['<a id="a"><b id="b">hey!</b></a>'], { type: "text/html"}))
564 * .end(callback);
565 * ```
566 *
567 * @param {String} field
568 * @param {Blob|File} file
569 * @param {String|Object} options
570 * @return {Request} for chaining
571 * @api public
572 */
573
574Request.prototype.attach = function(field, file, options){
575 if (file) {
576 if (this._data) {
577 throw Error("superagent can't mix .send() and .attach()");
578 }
579
580 this._getFormData().append(field, file, options || file.name);
581 }
582 return this;
583};
584
585Request.prototype._getFormData = function(){
586 if (!this._formData) {
587 this._formData = new root.FormData();
588 }
589 return this._formData;
590};
591
592/**
593 * Invoke the callback with `err` and `res`
594 * and handle arity check.
595 *
596 * @param {Error} err
597 * @param {Response} res
598 * @api private
599 */
600
601Request.prototype.callback = function(err, res){
602 // console.log(this._retries, this._maxRetries)
603 if (this._maxRetries && this._retries++ < this._maxRetries && shouldRetry(err, res)) {
604 return this._retry();
605 }
606
607 var fn = this._callback;
608 this.clearTimeout();
609
610 if (err) {
611 if (this._maxRetries) err.retries = this._retries - 1;
612 this.emit('error', err);
613 }
614
615 fn(err, res);
616};
617
618/**
619 * Invoke callback with x-domain error.
620 *
621 * @api private
622 */
623
624Request.prototype.crossDomainError = function(){
625 var err = new Error('Request has been terminated\nPossible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.');
626 err.crossDomain = true;
627
628 err.status = this.status;
629 err.method = this.method;
630 err.url = this.url;
631
632 this.callback(err);
633};
634
635// This only warns, because the request is still likely to work
636Request.prototype.buffer = Request.prototype.ca = Request.prototype.agent = function(){
637 console.warn("This is not supported in browser version of superagent");
638 return this;
639};
640
641// This throws, because it can't send/receive data as expected
642Request.prototype.pipe = Request.prototype.write = function(){
643 throw Error("Streaming is not supported in browser version of superagent");
644};
645
646/**
647 * Compose querystring to append to req.url
648 *
649 * @api private
650 */
651
652Request.prototype._appendQueryString = function(){
653 var query = this._query.join('&');
654 if (query) {
655 this.url += (this.url.indexOf('?') >= 0 ? '&' : '?') + query;
656 }
657
658 if (this._sort) {
659 var index = this.url.indexOf('?');
660 if (index >= 0) {
661 var queryArr = this.url.substring(index + 1).split('&');
662 if (isFunction(this._sort)) {
663 queryArr.sort(this._sort);
664 } else {
665 queryArr.sort();
666 }
667 this.url = this.url.substring(0, index) + '?' + queryArr.join('&');
668 }
669 }
670};
671
672/**
673 * Check if `obj` is a host object,
674 * we don't want to serialize these :)
675 *
676 * @param {Object} obj
677 * @return {Boolean}
678 * @api private
679 */
680Request.prototype._isHost = function _isHost(obj) {
681 // Native objects stringify to [object File], [object Blob], [object FormData], etc.
682 return obj && 'object' === typeof obj && !Array.isArray(obj) && Object.prototype.toString.call(obj) !== '[object Object]';
683}
684
685/**
686 * Initiate request, invoking callback `fn(res)`
687 * with an instanceof `Response`.
688 *
689 * @param {Function} fn
690 * @return {Request} for chaining
691 * @api public
692 */
693
694Request.prototype.end = function(fn){
695 if (this._endCalled) {
696 console.warn("Warning: .end() was called twice. This is not supported in superagent");
697 }
698 this._endCalled = true;
699
700 // store callback
701 this._callback = fn || noop;
702
703 // querystring
704 this._appendQueryString();
705
706 return this._end();
707};
708
709Request.prototype._end = function() {
710 var self = this;
711 var xhr = this.xhr = request.getXHR();
712 var data = this._formData || this._data;
713
714 this._setTimeouts();
715
716 // state change
717 xhr.onreadystatechange = function(){
718 var readyState = xhr.readyState;
719 if (readyState >= 2 && self._responseTimeoutTimer) {
720 clearTimeout(self._responseTimeoutTimer);
721 }
722 if (4 != readyState) {
723 return;
724 }
725
726 // In IE9, reads to any property (e.g. status) off of an aborted XHR will
727 // result in the error "Could not complete the operation due to error c00c023f"
728 var status;
729 try { status = xhr.status } catch(e) { status = 0; }
730
731 if (!status) {
732 if (self.timedout || self._aborted) return;
733 return self.crossDomainError();
734 }
735 self.emit('end');
736 };
737
738 // progress
739 var handleProgress = function(direction, e) {
740 if (e.total > 0) {
741 e.percent = e.loaded / e.total * 100;
742 }
743 e.direction = direction;
744 self.emit('progress', e);
745 }
746 if (this.hasListeners('progress')) {
747 try {
748 xhr.onprogress = handleProgress.bind(null, 'download');
749 if (xhr.upload) {
750 xhr.upload.onprogress = handleProgress.bind(null, 'upload');
751 }
752 } catch(e) {
753 // Accessing xhr.upload fails in IE from a web worker, so just pretend it doesn't exist.
754 // Reported here:
755 // https://connect.microsoft.com/IE/feedback/details/837245/xmlhttprequest-upload-throws-invalid-argument-when-used-from-web-worker-context
756 }
757 }
758
759 // initiate request
760 try {
761 if (this.username && this.password) {
762 xhr.open(this.method, this.url, true, this.username, this.password);
763 } else {
764 xhr.open(this.method, this.url, true);
765 }
766 } catch (err) {
767 // see #1149
768 return this.callback(err);
769 }
770
771 // CORS
772 if (this._withCredentials) xhr.withCredentials = true;
773
774 // body
775 if (!this._formData && 'GET' != this.method && 'HEAD' != this.method && 'string' != typeof data && !this._isHost(data)) {
776 // serialize stuff
777 var contentType = this._header['content-type'];
778 var serialize = this._serializer || request.serialize[contentType ? contentType.split(';')[0] : ''];
779 if (!serialize && isJSON(contentType)) {
780 serialize = request.serialize['application/json'];
781 }
782 if (serialize) data = serialize(data);
783 }
784
785 // set header fields
786 for (var field in this.header) {
787 if (null == this.header[field]) continue;
788
789 if (this.header.hasOwnProperty(field))
790 xhr.setRequestHeader(field, this.header[field]);
791 }
792
793 if (this._responseType) {
794 xhr.responseType = this._responseType;
795 }
796
797 // send stuff
798 this.emit('request', this);
799
800 // IE11 xhr.send(undefined) sends 'undefined' string as POST payload (instead of nothing)
801 // We need null here if data is undefined
802 xhr.send(typeof data !== 'undefined' ? data : null);
803 return this;
804};
805
806/**
807 * GET `url` with optional callback `fn(res)`.
808 *
809 * @param {String} url
810 * @param {Mixed|Function} [data] or fn
811 * @param {Function} [fn]
812 * @return {Request}
813 * @api public
814 */
815
816request.get = function(url, data, fn){
817 var req = request('GET', url);
818 if ('function' == typeof data) fn = data, data = null;
819 if (data) req.query(data);
820 if (fn) req.end(fn);
821 return req;
822};
823
824/**
825 * HEAD `url` with optional callback `fn(res)`.
826 *
827 * @param {String} url
828 * @param {Mixed|Function} [data] or fn
829 * @param {Function} [fn]
830 * @return {Request}
831 * @api public
832 */
833
834request.head = function(url, data, fn){
835 var req = request('HEAD', url);
836 if ('function' == typeof data) fn = data, data = null;
837 if (data) req.send(data);
838 if (fn) req.end(fn);
839 return req;
840};
841
842/**
843 * OPTIONS query to `url` with optional callback `fn(res)`.
844 *
845 * @param {String} url
846 * @param {Mixed|Function} [data] or fn
847 * @param {Function} [fn]
848 * @return {Request}
849 * @api public
850 */
851
852request.options = function(url, data, fn){
853 var req = request('OPTIONS', url);
854 if ('function' == typeof data) fn = data, data = null;
855 if (data) req.send(data);
856 if (fn) req.end(fn);
857 return req;
858};
859
860/**
861 * DELETE `url` with optional `data` and callback `fn(res)`.
862 *
863 * @param {String} url
864 * @param {Mixed} [data]
865 * @param {Function} [fn]
866 * @return {Request}
867 * @api public
868 */
869
870function del(url, data, fn){
871 var req = request('DELETE', url);
872 if ('function' == typeof data) fn = data, data = null;
873 if (data) req.send(data);
874 if (fn) req.end(fn);
875 return req;
876};
877
878request['del'] = del;
879request['delete'] = del;
880
881/**
882 * PATCH `url` with optional `data` and callback `fn(res)`.
883 *
884 * @param {String} url
885 * @param {Mixed} [data]
886 * @param {Function} [fn]
887 * @return {Request}
888 * @api public
889 */
890
891request.patch = function(url, data, fn){
892 var req = request('PATCH', url);
893 if ('function' == typeof data) fn = data, data = null;
894 if (data) req.send(data);
895 if (fn) req.end(fn);
896 return req;
897};
898
899/**
900 * POST `url` with optional `data` and callback `fn(res)`.
901 *
902 * @param {String} url
903 * @param {Mixed} [data]
904 * @param {Function} [fn]
905 * @return {Request}
906 * @api public
907 */
908
909request.post = function(url, data, fn){
910 var req = request('POST', url);
911 if ('function' == typeof data) fn = data, data = null;
912 if (data) req.send(data);
913 if (fn) req.end(fn);
914 return req;
915};
916
917/**
918 * PUT `url` with optional `data` and callback `fn(res)`.
919 *
920 * @param {String} url
921 * @param {Mixed|Function} [data] or fn
922 * @param {Function} [fn]
923 * @return {Request}
924 * @api public
925 */
926
927request.put = function(url, data, fn){
928 var req = request('PUT', url);
929 if ('function' == typeof data) fn = data, data = null;
930 if (data) req.send(data);
931 if (fn) req.end(fn);
932 return req;
933};
934
\No newline at end of file