UNPKG

13 kBJavaScriptView Raw
1/**
2 * Module of mixed-in functions shared between node and client code
3 */
4var isObject = require('./is-object');
5
6/**
7 * Expose `RequestBase`.
8 */
9
10module.exports = RequestBase;
11
12/**
13 * Initialize a new `RequestBase`.
14 *
15 * @api public
16 */
17
18function RequestBase(obj) {
19 if (obj) return mixin(obj);
20}
21
22/**
23 * Mixin the prototype properties.
24 *
25 * @param {Object} obj
26 * @return {Object}
27 * @api private
28 */
29
30function mixin(obj) {
31 for (var key in RequestBase.prototype) {
32 obj[key] = RequestBase.prototype[key];
33 }
34 return obj;
35}
36
37/**
38 * Clear previous timeout.
39 *
40 * @return {Request} for chaining
41 * @api public
42 */
43
44RequestBase.prototype.clearTimeout = function _clearTimeout(){
45 clearTimeout(this._timer);
46 clearTimeout(this._responseTimeoutTimer);
47 delete this._timer;
48 delete this._responseTimeoutTimer;
49 return this;
50};
51
52/**
53 * Override default response body parser
54 *
55 * This function will be called to convert incoming data into request.body
56 *
57 * @param {Function}
58 * @api public
59 */
60
61RequestBase.prototype.parse = function parse(fn){
62 this._parser = fn;
63 return this;
64};
65
66/**
67 * Set format of binary response body.
68 * In browser valid formats are 'blob' and 'arraybuffer',
69 * which return Blob and ArrayBuffer, respectively.
70 *
71 * In Node all values result in Buffer.
72 *
73 * Examples:
74 *
75 * req.get('/')
76 * .responseType('blob')
77 * .end(callback);
78 *
79 * @param {String} val
80 * @return {Request} for chaining
81 * @api public
82 */
83
84RequestBase.prototype.responseType = function(val){
85 this._responseType = val;
86 return this;
87};
88
89/**
90 * Override default request body serializer
91 *
92 * This function will be called to convert data set via .send or .attach into payload to send
93 *
94 * @param {Function}
95 * @api public
96 */
97
98RequestBase.prototype.serialize = function serialize(fn){
99 this._serializer = fn;
100 return this;
101};
102
103/**
104 * Set timeouts.
105 *
106 * - response timeout is time between sending request and receiving the first byte of the response. Includes DNS and connection time.
107 * - deadline is the time from start of the request to receiving response body in full. If the deadline is too short large files may not load at all on slow connections.
108 *
109 * Value of 0 or false means no timeout.
110 *
111 * @param {Number|Object} ms or {response, read, deadline}
112 * @return {Request} for chaining
113 * @api public
114 */
115
116RequestBase.prototype.timeout = function timeout(options){
117 if (!options || 'object' !== typeof options) {
118 this._timeout = options;
119 this._responseTimeout = 0;
120 return this;
121 }
122
123 for(var option in options) {
124 switch(option) {
125 case 'deadline':
126 this._timeout = options.deadline;
127 break;
128 case 'response':
129 this._responseTimeout = options.response;
130 break;
131 default:
132 console.warn("Unknown timeout option", option);
133 }
134 }
135 return this;
136};
137
138/**
139 * Set number of retry attempts on error.
140 *
141 * Failed requests will be retried 'count' times if timeout or err.code >= 500.
142 *
143 * @param {Number} count
144 * @return {Request} for chaining
145 * @api public
146 */
147
148RequestBase.prototype.retry = function retry(count){
149 // Default to 1 if no count passed or true
150 if (arguments.length === 0 || count === true) count = 1;
151 if (count <= 0) count = 0;
152 this._maxRetries = count;
153 this._retries = 0;
154 return this;
155};
156
157/**
158 * Retry request
159 *
160 * @return {Request} for chaining
161 * @api private
162 */
163
164RequestBase.prototype._retry = function() {
165 this.clearTimeout();
166
167 // node
168 if (this.req) {
169 this.req = null;
170 this.req = this.request();
171 }
172
173 this._aborted = false;
174 this.timedout = false;
175
176 return this._end();
177};
178
179/**
180 * Promise support
181 *
182 * @param {Function} resolve
183 * @param {Function} [reject]
184 * @return {Request}
185 */
186
187RequestBase.prototype.then = function then(resolve, reject) {
188 if (!this._fullfilledPromise) {
189 var self = this;
190 if (this._endCalled) {
191 console.warn("Warning: superagent request was sent twice, because both .end() and .then() were called. Never call .end() if you use promises");
192 }
193 this._fullfilledPromise = new Promise(function(innerResolve, innerReject){
194 self.end(function(err, res){
195 if (err) innerReject(err); else innerResolve(res);
196 });
197 });
198 }
199 return this._fullfilledPromise.then(resolve, reject);
200}
201
202RequestBase.prototype.catch = function(cb) {
203 return this.then(undefined, cb);
204};
205
206/**
207 * Allow for extension
208 */
209
210RequestBase.prototype.use = function use(fn) {
211 fn(this);
212 return this;
213}
214
215RequestBase.prototype.ok = function(cb) {
216 if ('function' !== typeof cb) throw Error("Callback required");
217 this._okCallback = cb;
218 return this;
219};
220
221RequestBase.prototype._isResponseOK = function(res) {
222 if (!res) {
223 return false;
224 }
225
226 if (this._okCallback) {
227 return this._okCallback(res);
228 }
229
230 return res.status >= 200 && res.status < 300;
231};
232
233
234/**
235 * Get request header `field`.
236 * Case-insensitive.
237 *
238 * @param {String} field
239 * @return {String}
240 * @api public
241 */
242
243RequestBase.prototype.get = function(field){
244 return this._header[field.toLowerCase()];
245};
246
247/**
248 * Get case-insensitive header `field` value.
249 * This is a deprecated internal API. Use `.get(field)` instead.
250 *
251 * (getHeader is no longer used internally by the superagent code base)
252 *
253 * @param {String} field
254 * @return {String}
255 * @api private
256 * @deprecated
257 */
258
259RequestBase.prototype.getHeader = RequestBase.prototype.get;
260
261/**
262 * Set header `field` to `val`, or multiple fields with one object.
263 * Case-insensitive.
264 *
265 * Examples:
266 *
267 * req.get('/')
268 * .set('Accept', 'application/json')
269 * .set('X-API-Key', 'foobar')
270 * .end(callback);
271 *
272 * req.get('/')
273 * .set({ Accept: 'application/json', 'X-API-Key': 'foobar' })
274 * .end(callback);
275 *
276 * @param {String|Object} field
277 * @param {String} val
278 * @return {Request} for chaining
279 * @api public
280 */
281
282RequestBase.prototype.set = function(field, val){
283 if (isObject(field)) {
284 for (var key in field) {
285 this.set(key, field[key]);
286 }
287 return this;
288 }
289 this._header[field.toLowerCase()] = val;
290 this.header[field] = val;
291 return this;
292};
293
294/**
295 * Remove header `field`.
296 * Case-insensitive.
297 *
298 * Example:
299 *
300 * req.get('/')
301 * .unset('User-Agent')
302 * .end(callback);
303 *
304 * @param {String} field
305 */
306RequestBase.prototype.unset = function(field){
307 delete this._header[field.toLowerCase()];
308 delete this.header[field];
309 return this;
310};
311
312/**
313 * Write the field `name` and `val`, or multiple fields with one object
314 * for "multipart/form-data" request bodies.
315 *
316 * ``` js
317 * request.post('/upload')
318 * .field('foo', 'bar')
319 * .end(callback);
320 *
321 * request.post('/upload')
322 * .field({ foo: 'bar', baz: 'qux' })
323 * .end(callback);
324 * ```
325 *
326 * @param {String|Object} name
327 * @param {String|Blob|File|Buffer|fs.ReadStream} val
328 * @return {Request} for chaining
329 * @api public
330 */
331RequestBase.prototype.field = function(name, val) {
332
333 // name should be either a string or an object.
334 if (null === name || undefined === name) {
335 throw new Error('.field(name, val) name can not be empty');
336 }
337
338 if (this._data) {
339 console.error(".field() can't be used if .send() is used. Please use only .send() or only .field() & .attach()");
340 }
341
342 if (isObject(name)) {
343 for (var key in name) {
344 this.field(key, name[key]);
345 }
346 return this;
347 }
348
349 if (Array.isArray(val)) {
350 for (var i in val) {
351 this.field(name, val[i]);
352 }
353 return this;
354 }
355
356 // val should be defined now
357 if (null === val || undefined === val) {
358 throw new Error('.field(name, val) val can not be empty');
359 }
360 if ('boolean' === typeof val) {
361 val = '' + val;
362 }
363 this._getFormData().append(name, val);
364 return this;
365};
366
367/**
368 * Abort the request, and clear potential timeout.
369 *
370 * @return {Request}
371 * @api public
372 */
373RequestBase.prototype.abort = function(){
374 if (this._aborted) {
375 return this;
376 }
377 this._aborted = true;
378 this.xhr && this.xhr.abort(); // browser
379 this.req && this.req.abort(); // node
380 this.clearTimeout();
381 this.emit('abort');
382 return this;
383};
384
385/**
386 * Enable transmission of cookies with x-domain requests.
387 *
388 * Note that for this to work the origin must not be
389 * using "Access-Control-Allow-Origin" with a wildcard,
390 * and also must set "Access-Control-Allow-Credentials"
391 * to "true".
392 *
393 * @api public
394 */
395
396RequestBase.prototype.withCredentials = function(on){
397 // This is browser-only functionality. Node side is no-op.
398 if(on==undefined) on = true;
399 this._withCredentials = on;
400 return this;
401};
402
403/**
404 * Set the max redirects to `n`. Does noting in browser XHR implementation.
405 *
406 * @param {Number} n
407 * @return {Request} for chaining
408 * @api public
409 */
410
411RequestBase.prototype.redirects = function(n){
412 this._maxRedirects = n;
413 return this;
414};
415
416/**
417 * Convert to a plain javascript object (not JSON string) of scalar properties.
418 * Note as this method is designed to return a useful non-this value,
419 * it cannot be chained.
420 *
421 * @return {Object} describing method, url, and data of this request
422 * @api public
423 */
424
425RequestBase.prototype.toJSON = function(){
426 return {
427 method: this.method,
428 url: this.url,
429 data: this._data,
430 headers: this._header
431 };
432};
433
434
435/**
436 * Send `data` as the request body, defaulting the `.type()` to "json" when
437 * an object is given.
438 *
439 * Examples:
440 *
441 * // manual json
442 * request.post('/user')
443 * .type('json')
444 * .send('{"name":"tj"}')
445 * .end(callback)
446 *
447 * // auto json
448 * request.post('/user')
449 * .send({ name: 'tj' })
450 * .end(callback)
451 *
452 * // manual x-www-form-urlencoded
453 * request.post('/user')
454 * .type('form')
455 * .send('name=tj')
456 * .end(callback)
457 *
458 * // auto x-www-form-urlencoded
459 * request.post('/user')
460 * .type('form')
461 * .send({ name: 'tj' })
462 * .end(callback)
463 *
464 * // defaults to x-www-form-urlencoded
465 * request.post('/user')
466 * .send('name=tobi')
467 * .send('species=ferret')
468 * .end(callback)
469 *
470 * @param {String|Object} data
471 * @return {Request} for chaining
472 * @api public
473 */
474
475RequestBase.prototype.send = function(data){
476 var isObj = isObject(data);
477 var type = this._header['content-type'];
478
479 if (this._formData) {
480 console.error(".send() can't be used if .attach() or .field() is used. Please use only .send() or only .field() & .attach()");
481 }
482
483 if (isObj && !this._data) {
484 if (Array.isArray(data)) {
485 this._data = [];
486 } else if (!this._isHost(data)) {
487 this._data = {};
488 }
489 } else if (data && this._data && this._isHost(this._data)) {
490 throw Error("Can't merge these send calls");
491 }
492
493 // merge
494 if (isObj && isObject(this._data)) {
495 for (var key in data) {
496 this._data[key] = data[key];
497 }
498 } else if ('string' == typeof data) {
499 // default to x-www-form-urlencoded
500 if (!type) this.type('form');
501 type = this._header['content-type'];
502 if ('application/x-www-form-urlencoded' == type) {
503 this._data = this._data
504 ? this._data + '&' + data
505 : data;
506 } else {
507 this._data = (this._data || '') + data;
508 }
509 } else {
510 this._data = data;
511 }
512
513 if (!isObj || this._isHost(data)) {
514 return this;
515 }
516
517 // default to json
518 if (!type) this.type('json');
519 return this;
520};
521
522
523/**
524 * Sort `querystring` by the sort function
525 *
526 *
527 * Examples:
528 *
529 * // default order
530 * request.get('/user')
531 * .query('name=Nick')
532 * .query('search=Manny')
533 * .sortQuery()
534 * .end(callback)
535 *
536 * // customized sort function
537 * request.get('/user')
538 * .query('name=Nick')
539 * .query('search=Manny')
540 * .sortQuery(function(a, b){
541 * return a.length - b.length;
542 * })
543 * .end(callback)
544 *
545 *
546 * @param {Function} sort
547 * @return {Request} for chaining
548 * @api public
549 */
550
551RequestBase.prototype.sortQuery = function(sort) {
552 // _sort default to true but otherwise can be a function or boolean
553 this._sort = typeof sort === 'undefined' ? true : sort;
554 return this;
555};
556
557/**
558 * Invoke callback with timeout error.
559 *
560 * @api private
561 */
562
563RequestBase.prototype._timeoutError = function(reason, timeout, errno){
564 if (this._aborted) {
565 return;
566 }
567 var err = new Error(reason + timeout + 'ms exceeded');
568 err.timeout = timeout;
569 err.code = 'ECONNABORTED';
570 err.errno = errno;
571 this.timedout = true;
572 this.abort();
573 this.callback(err);
574};
575
576RequestBase.prototype._setTimeouts = function() {
577 var self = this;
578
579 // deadline
580 if (this._timeout && !this._timer) {
581 this._timer = setTimeout(function(){
582 self._timeoutError('Timeout of ', self._timeout, 'ETIME');
583 }, this._timeout);
584 }
585 // response timeout
586 if (this._responseTimeout && !this._responseTimeoutTimer) {
587 this._responseTimeoutTimer = setTimeout(function(){
588 self._timeoutError('Response timeout of ', self._responseTimeout, 'ETIMEDOUT');
589 }, this._responseTimeout);
590 }
591}