├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── lib ├── index.js └── retries.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.6.0 / 2016-05-16 3 | ================== 4 | 5 | * [FEATURE] Retry on 500 errors 6 | 7 | 0.5.1 / 2014-10-08 8 | ================== 9 | 10 | * retain request path 11 | * downgrade superagent for backfilling fix 12 | 13 | 0.5.0 / 2014-10-03 14 | ================== 15 | 16 | * upgrade superagent to 0.20 17 | 18 | 0.4.0 / 2014-09-30 19 | ================== 20 | 21 | * update superagent 22 | 23 | 0.3.0 / 2014-04-30 24 | ================== 25 | 26 | * update superagent 27 | 28 | 0.2.0 / 2014-03-06 29 | ================== 30 | 31 | * re-set headers 32 | * update superagent 33 | 34 | 0.1.1 / 2013-10-23 35 | ================== 36 | 37 | * Added support for 502, 504 responses ([justincy](https://github.com/justincy)) 38 | 39 | 0.1.0 / 2013-09-03 40 | ================== 41 | 42 | * Initial Version 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --require should \ 5 | --reporter spec 6 | 7 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Unmaintained ⚠️ 2 | 3 | This repository is unmaintained, and this [functionality has been added to superagent core](https://visionmedia.github.io/superagent/#retrying-requests). 4 | 5 | # superagent-retry 6 | 7 | Extends the node version of [`visionmedia/superagent`][superagent]'s `Request`, adds a `.retry` method to add retrying logic to the request. Calling this will retry the request however many additional times you'd like. 8 | 9 | 10 | [superagent]: https://github.com/visionmedia/superagent 11 | 12 | ## Usage 13 | 14 | ```javascript 15 | var superagent = require('superagent'); 16 | require('superagent-retry')(superagent); 17 | 18 | superagent 19 | .get('https://segment.io') 20 | .retry(2) // retry twice before responding 21 | .end(onresponse); 22 | 23 | 24 | function onresponse (err, res) { 25 | console.log(res.status, res.headers); 26 | console.log(res.body); 27 | } 28 | ``` 29 | 30 | ## Retrying Cases 31 | 32 | Currently the retrying logic checks for: 33 | 34 | * ECONNRESET 35 | * ECONNREFUSED 36 | * ETIMEDOUT 37 | * EADDRINFO 38 | * ESOCKETTIMEDOUT 39 | * superagent client timeouts 40 | * bad gateway errors (502, 503, 504 statuses) 41 | * Internal Server Error (500 status) 42 | 43 | 44 | ## License 45 | 46 | (The MIT License) 47 | 48 | Copyright (c) 2013 Segmentio <friends@segment.io> 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining 51 | a copy of this software and associated documentation files (the 52 | 'Software'), to deal in the Software without restriction, including 53 | without limitation the rights to use, copy, modify, merge, publish, 54 | distribute, sublicense, and/or sell copies of the Software, and to 55 | permit persons to whom the Software is furnished to do so, subject to 56 | the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be 59 | included in all copies or substantial portions of the Software. 60 | 61 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 62 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 63 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 64 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 65 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 66 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 67 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 68 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var retries = require('./retries'); 2 | 3 | 4 | /** 5 | * Add to the request prototype. 6 | */ 7 | 8 | module.exports = function (superagent) { 9 | var Request = superagent.Request; 10 | Request.prototype.retry = retry; 11 | return superagent; 12 | }; 13 | 14 | 15 | /** 16 | * Export retries for extending 17 | */ 18 | 19 | module.exports.retries = retries; 20 | 21 | 22 | /** 23 | * Sets the amount of times to retry the request 24 | * @param {Number} count 25 | */ 26 | 27 | function retry (retries) { 28 | 29 | var self = this 30 | , oldEnd = this.end; 31 | 32 | retries = retries || 1; 33 | 34 | this.end = function (fn) { 35 | var timeout = this._timeout; 36 | 37 | function attemptRetry () { 38 | return oldEnd.call(self, function (err, res) { 39 | if (!retries || !shouldRetry(err, res)) return fn && fn(err, res); 40 | 41 | reset(self, timeout); 42 | 43 | retries--; 44 | return attemptRetry(); 45 | }); 46 | } 47 | 48 | return attemptRetry(); 49 | }; 50 | 51 | return this; 52 | } 53 | 54 | 55 | /** 56 | * HACK: Resets the internal state of a request. 57 | */ 58 | 59 | function reset (request, timeout) { 60 | var headers = request.req._headers; 61 | var path = request.req.path; 62 | 63 | request.req.abort(); 64 | request.called = false; 65 | request.timeout(timeout); 66 | delete request.req; 67 | delete request._timer; 68 | 69 | for (var k in headers) { 70 | request.set(k, headers[k]); 71 | } 72 | 73 | if (!request.qs) { 74 | request.req.path = path; 75 | } 76 | } 77 | 78 | 79 | /** 80 | * Determine whether we should retry based upon common error conditions 81 | * @param {Error} err 82 | * @param {Response} res 83 | * @return {Boolean} 84 | */ 85 | 86 | function shouldRetry (err, res) { 87 | return retries.some(function (check) { return check(err, res); }); 88 | } 89 | -------------------------------------------------------------------------------- /lib/retries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common retry conditions 3 | */ 4 | 5 | module.exports = [ 6 | econnreset, 7 | econnrefused, 8 | etimedout, 9 | eaddrinfo, 10 | esockettimedout, 11 | gateway, 12 | timeout, 13 | internal 14 | ]; 15 | 16 | 17 | /** 18 | * Connection reset detection 19 | */ 20 | 21 | function econnreset (err, res) { 22 | return err && err.code === 'ECONNRESET'; 23 | } 24 | 25 | /** 26 | * Connection refused detection 27 | */ 28 | 29 | function econnrefused (err, res) { 30 | return err && err.code === 'ECONNREFUSED'; 31 | } 32 | 33 | /** 34 | * Timeout detection 35 | */ 36 | 37 | function etimedout (err, res) { 38 | return err && err.code === 'ETIMEDOUT'; 39 | } 40 | 41 | 42 | /** 43 | * Can't get address info 44 | */ 45 | 46 | function eaddrinfo (err, res) { 47 | return err && err.code === 'EADDRINFO'; 48 | } 49 | 50 | 51 | /** 52 | * Socket timeout detection 53 | */ 54 | 55 | function esockettimedout (err, res) { 56 | return err && err.code === 'ESOCKETTIMEDOUT'; 57 | } 58 | 59 | /** 60 | * Internal server error 61 | */ 62 | 63 | function internal (err, res) { 64 | return res && res.status === 500; 65 | } 66 | 67 | /** 68 | * Bad gateway error detection 69 | */ 70 | 71 | function gateway (err, res) { 72 | return res && [502,503,504].indexOf(res.status) !== -1; 73 | } 74 | 75 | 76 | /** 77 | * Superagent timeout errors 78 | */ 79 | 80 | function timeout (err, res) { 81 | return err && /^timeout of \d+ms exceeded$/.test(err.message); 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superagent-retry", 3 | "version": "0.6.0", 4 | "description": "A retrying layer for a superagent request", 5 | "keywords": [ 6 | "superagent", 7 | "retry" 8 | ], 9 | "author": "Segmentio ", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/segmentio/superagent-retry.git" 13 | }, 14 | "dependencies": {}, 15 | "peerDependencies": { 16 | "superagent": "*" 17 | }, 18 | "devDependencies": { 19 | "mocha": "*", 20 | "should": "*", 21 | "express": "~3.3.8", 22 | "superagent": "~0.20.0" 23 | }, 24 | "main": "lib/index" 25 | } 26 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , agent = require('superagent') 3 | , should = require('should') 4 | , assert = require('assert') 5 | , http = require('http'); 6 | 7 | 8 | http.globalAgent.maxSockets = 2000; 9 | 10 | 11 | require('../')(agent); 12 | 13 | 14 | describe('superagent-retry', function () { 15 | 16 | describe('errors', function () { 17 | var requests = 0 18 | , port = 10410 19 | , app = express() 20 | , server; 21 | 22 | before(function (done) { 23 | app.get('/', function (req, res, next) { 24 | requests++; 25 | if (requests < 4) res.send(503); 26 | else res.send('hello!'); 27 | }); 28 | 29 | server = app.listen(port, done); 30 | }); 31 | 32 | it('should retry on errors', function (done) { 33 | 34 | agent 35 | .get('http://localhost:' + port) 36 | .end(function (err, res) { 37 | res.status.should.eql(503); 38 | }); 39 | 40 | setTimeout(function () { 41 | agent 42 | .get('http://localhost:' + port) 43 | .retry(5) 44 | .end(function (err, res) { 45 | res.text.should.eql('hello!'); 46 | requests.should.eql(4); 47 | done(err); 48 | }); 49 | }, 100); 50 | }); 51 | 52 | after(function (done) { server.close(done); }); 53 | }); 54 | 55 | describe('500 errors', function () { 56 | var requests = 0 57 | , port = 10410 58 | , app = express() 59 | , server; 60 | 61 | before(function (done) { 62 | app.get('/', function (req, res, next) { 63 | requests++; 64 | if (requests < 4) res.send(500); 65 | else res.send('hello!'); 66 | }); 67 | 68 | server = app.listen(port, done); 69 | }); 70 | 71 | it('should retry on errors', function (done) { 72 | 73 | agent 74 | .get('http://localhost:' + port) 75 | .end(function (err, res) { 76 | res.status.should.eql(500); 77 | }); 78 | 79 | setTimeout(function () { 80 | agent 81 | .get('http://localhost:' + port) 82 | .retry(5) 83 | .end(function (err, res) { 84 | console.log('requests', requests) 85 | res.text.should.eql('hello!'); 86 | requests.should.eql(4); 87 | done(err); 88 | }); 89 | }, 100); 90 | }); 91 | 92 | after(function (done) { server.close(done); }); 93 | }); 94 | 95 | describe('resets', function () { 96 | var requests = 0 97 | , port = 10410 98 | , app = express() 99 | , server; 100 | 101 | before(function (done) { 102 | server = app.listen(port, done); 103 | }); 104 | 105 | it('should retry client timeouts', function (done) { 106 | app.get('/client-timeouts', function (req, res, next) { 107 | requests++; 108 | if (requests > 10) res.send('hello!'); 109 | }); 110 | 111 | var url = 'http://localhost:' + port + '/client-timeouts'; 112 | 113 | agent 114 | .get(url) 115 | .timeout(10) 116 | .end(function (err, res) { 117 | should.exist(err); 118 | }); 119 | 120 | agent 121 | .get(url) 122 | .timeout(2) 123 | .retry(20) 124 | .end(function (err, res) { 125 | res.text.should.eql('hello!'); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('should retry with the same headers', function(done){ 131 | var url = 'http://localhost:' + port + '/headers'; 132 | var requests = 0; 133 | 134 | app.get('/headers', function(req, res){ 135 | if (++requests > 3) return res.send(req.headers); 136 | }); 137 | 138 | agent 139 | .get(url) 140 | .set('Accept', 'application/json') 141 | .set('X-Foo', 'baz') 142 | .timeout(10) 143 | .retry(4) 144 | .end(function(err, res){ 145 | assert('baz' == res.body['x-foo']); 146 | assert('application/json' == res.body['accept']); 147 | done(); 148 | }); 149 | }) 150 | 151 | it('should re-send data and headers correctly', function(done){ 152 | var url = 'http://localhost:' + port + '/data'; 153 | var requests = 0; 154 | 155 | app.post('/data', express.bodyParser(), function(req, res){ 156 | if (++requests < 3) return; 157 | res.send({ body: req.body, headers: req.headers }); 158 | }); 159 | 160 | agent 161 | .post(url) 162 | .type('json') 163 | .send({ data: 1 }) 164 | .timeout(10) 165 | .retry(4) 166 | .end(function(err, res){ 167 | assert(1 == res.body.body.data); 168 | assert('application/json' == res.body.headers['content-type']); 169 | done(); 170 | }); 171 | }) 172 | 173 | it('should retry on server resets', function (done) { 174 | var requests = 0; 175 | 176 | app.get('/server-timeouts', function (req, res, next) { 177 | requests++; 178 | if (requests > 10) return res.send('hello!'); 179 | res.setTimeout(1); 180 | }); 181 | 182 | var url = 'http://localhost:' + port + '/server-timeouts'; 183 | 184 | agent 185 | .get(url) 186 | .end(function (err, res) { 187 | err.code.should.eql('ECONNRESET'); 188 | }); 189 | 190 | agent 191 | .get(url) 192 | .retry(20) 193 | .end(function (err, res) { 194 | res.text.should.eql('hello!'); 195 | done(); 196 | }); 197 | }); 198 | 199 | it('should retry on server connection refused', function (done) { 200 | var url = 'http://localhost:' + (port+1) + '/hello'; 201 | var request = agent.get(url); 202 | var allowedRetries = 10; 203 | var allowedTries = allowedRetries + 1; 204 | var triesCount = 0 205 | 206 | var oldEnd = request.end; 207 | request.end = function(fn) { 208 | triesCount++; 209 | oldEnd.call(request, fn); 210 | }; 211 | 212 | request 213 | .retry(allowedRetries) 214 | .end(function (err, res) { 215 | err.code.should.eql('ECONNREFUSED'); 216 | triesCount.should.eql(allowedTries); 217 | done(); 218 | }); 219 | }); 220 | 221 | it('should retry with the same querystring', function(done){ 222 | var requests = 0; 223 | 224 | app.get('/qs-data', function(req, res){ 225 | if (++requests > 10) return res.json({ foo: req.query.foo }); 226 | res.setTimeout(1); 227 | }); 228 | 229 | var url = 'http://localhost:' + port + '/qs-data'; 230 | 231 | agent 232 | .get(url) 233 | .retry(20) 234 | .query({ foo: 'bar' }) 235 | .end(function(err, res){ 236 | res.body.foo.should.eql('bar'); 237 | done(); 238 | }) 239 | }); 240 | }); 241 | }) 242 | --------------------------------------------------------------------------------