Git Product home page Git Product logo

node-request-retry's Introduction


request-retry - HTTP(s) request retry on recoverable errors.


Coverage Status NPM version Downloads

Get help on Codementor available-for-advisory extra Slack

When the connection fails with one of ECONNRESET, ENOTFOUND, ESOCKETTIMEDOUT, ETIMEDOUT, ECONNREFUSED, EHOSTUNREACH, EPIPE, EAI_AGAIN or when an HTTP 5xx or 429 error occurrs, the request will automatically be re-attempted as these are often recoverable errors and will go away on retry.

❤️ Shameless plug

Installation

Install with npm.

npm install --save requestretry

Usage

Request-retry is a drop-in replacement for request but adds three new options maxAttempts, retryDelay and retryStrategy. It also adds one property to the response (or the error object, upon a network error), attempts. It supports callbacks or promises.

With callbacks

var request = require('requestretry');

request({
  url: 'https://api.domain.com/v1/a/b',
  json: true,

  // The below parameters are specific to request-retry
  maxAttempts: 5,   // (default) try 5 times
  retryDelay: 5000,  // (default) wait for 5s before trying again
  retryStrategy: request.RetryStrategies.HTTPOrNetworkError // (default) retry on 5xx or network errors
}, function(err, response, body){
  // this callback will only be called when the request succeeded or after maxAttempts or on error
  if (response) {
    console.log('The number of request attempts: ' + response.attempts);
  }
});

With promises

When you're using promises, you can pass the two following options:

  • fullResponse (default true) - To resolve the promise with the full response or just the body
  • promiseFactory (default whenjs) - A function to allow the usage of a different promise implementation library
request({
  url: 'https://api.domain.com/v1/a/b',
  json: true,

  fullResponse: true // (default) To resolve the promise with the full response or just the body
})
.then(function (response) {
  // response = The full response object or just the body
})
.catch(function(error) {
  // error = Any occurred error
})

Using promiseFactory option to use a different promise implementation library

// See the tests for different libraries usage examples

/**
 * @param  {Function} resolver The promise resolver function
 * @return {Object} The promise instance
 */
function customPromiseFactory(resolver) {
  // With when.js
  return require('when').promise(resolver);

  // With RSVP.js
  var Promise = require('rsvp').Promise;

  return new Promise(resolver);
}

request({
  url: 'https://api.domain.com/v1/a/b',
  json: true,

  // Custom promise factory function
  promiseFactory: customPromiseFactory
})
.then(function (response) {
  // response = The full response object or just the body
})
.catch(function(error) {
  // error = Any occurred error
})

How to define your own retry strategy

A retry strategy let you specify when request-retry should retry a request

/**
 * @param  {Null | Object} err
 * @param  {Object} response
 * @param  {Object} body
 * @param  {Object} options copy 
 * @return {Boolean} true if the request should be retried
 */
function myRetryStrategy(err, response, body, options){
  // retry the request if we had an error or if the response was a 'Bad Gateway'
  return !!err || response.statusCode === 502;
}

/**
 * @param  {Null | Object} err
 * @param  {Object} response
 * @param  {Object} body
 * @param  {Object} options copy 
 * @return {Object} mustRetry: {Boolean} true if the request should be retried
 *                  options: {Object} new options for request
 */
function myRetryStrategy(err, response, body, options){
  options.url = 'new url'; //you can overwrite some attributes or create new object 
  return {
    mustRetry: !!err || response.statusCode === 502,
    options: options, //then it should be passed back, it will be used for new requests
  }
}

/**
 * With an asynchronous retry strategy
 * @param  {Null | Object} err
 * @param  {Object} response
 * @param  {Object} body
 * @param  {Object} options copy 
 * @return {Object} mustRetry: {Boolean} true if the request should be retried
 *                  options: {Object} new options for request
 */
async function myRetryStrategy(err, response, body, options){
  let token = await getNewApiAuthToken();
  options.headers = {'Authorization': `Bearer ${token}`}
  return {
    mustRetry: true,
    options: options, // retry with new auth token
  }
}

request({
  url: 'https://api.domain.com/v1/a/b'
  json:true,
  retryStrategy: myRetryStrategy
}, function(err, response, body){
  // this callback will only be called when the request succeeded or after maxAttempts or on error
});

How to define your own delay strategy

A delay strategy let you specify how long request-retry should wait before trying again the request

/**
 * @param  {Null | Object} err
 * @param  {Object} response
 * @param  {Object} body
 * @return {Number} number of milliseconds to wait before trying again the request
 */
function myDelayStrategy(err, response, body){
  // set delay of retry to a random number between 500 and 3500 ms
  return Math.floor(Math.random() * (3500 - 500 + 1) + 500);
}

request({
  url: 'https://api.domain.com/v1/a/b'
  json:true,
  delayStrategy: myDelayStrategy // delayStrategy is called 1 less times than the maxAttempts set
}, function(err, response, body){
  // this callback will only be called when the request succeeded or after maxAttempts or on error
});

Here is how to implement an exponential backoff strategy:

/**
 * @param   {Number} attempts The number of times that the request has been attempted.
 * @return  {Number} number of milliseconds to wait before retrying again the request.
 */
function getExponentialBackoff(attempts) {
  return (Math.pow(2, attempts) * 100) + Math.floor(Math.random() * 50);
}

function constructExponentialBackoffStrategy() {
  let attempts = 0;
  return () => {
    attempts += 1;
    return getExponentialBackoff(attempts);
  };
}

request({
  url: 'https://api.domain.com/v1/a/b'
  json:true,
  delayStrategy: constructExponentialBackoffStrategy() // need to invoke the function to return the closure.
}, function(err, response, body){
  // this callback will only be called when the request succeeded or after maxAttempts or on error
});

How to access the underlying request library

You can access to the underlying request library thanks to request.Request:

const request = require('requestretry');
console.log(request.Request); // original request library

Thus, if needed, it's possible to monkey-patch or extend the underlying Request library:

request.Request = class extends request.Request {
  constructor(url, options, f, retryConfig) {
    super(url, options, f, retryConfig);
    // this constructor will be called for every requestretry call,
    // and give you global logging
    console.log('Request', url, options, f, retryConfig);
  }
}

Modifying request options

You can use the defaults method to provide default options like so:

var request = require('requestretry').defaults({ json: true, retryStrategy: myRetryStrategy });

API surface

As with request, several helpers are provided for various HTTP methods and usage:

  • request(options [, callback]).
  • request(url [, callback]) - same as request(options [, callback]).
  • request(url, options [, callback]) - same as request(options [, callback]).
  • request.get(url [, callback]) - same as request(options [, callback]), defaults options.method to GET.
  • request.get(url, options [, callback]) - same as request(options [, callback]), defaults options.method to GET.
  • request.head(url) - same as request(options [, callback]), defaults options.method to HEAD.
  • request.post(url) - same as request(options [, callback]), defaults options.method to POST.
  • request.put(url) - same as request(options [, callback]), defaults options.method to PUT.
  • request.patch(url) - same as request(options [, callback]), defaults options.method to PATCH.
  • request.del(url) - same as request(options [, callback]), defaults options.method to DELETE.
  • request.delete(url) - same as request(options [, callback]), defaults options.method to DELETE.

You want to support my work?

I maintain this project in my free time, if it helped you, well, I would be grateful to buy a beer thanks to your paypal or Bitcoins, donation!

Francois-Guillaume Ribreau ([email protected])

node-request-retry's People

Contributors

amrib24 avatar dgkanatsios avatar emmansun avatar evilaliv3 avatar eyepulp avatar fgribreau avatar fhemberger avatar greenkeeperio-bot avatar gw0 avatar juliendangers avatar markandrus avatar markstos avatar milsosa avatar misha-ridge avatar petar-iv avatar pwmckenna avatar rlsf avatar sampaguitas avatar sidneyjiang avatar smarth55 avatar sshadmand avatar stuartf avatar tcort avatar themusicroob avatar vioint avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

node-request-retry's Issues

Support for request default options

This is an extension of issue 8.

Doing the recommended method results in a TypeError:

var r = require('requestretry').request.defaults({pool: false});
TypeError: Cannot read property 'defaults' of undefined
    at repl:1:40
    at REPLServer.defaultEval (repl.js:132:27)
    at bound (domain.js:254:14)
    at REPLServer.runBound [as eval] (domain.js:267:12)
    at REPLServer.<anonymous> (repl.js:279:12)
    at REPLServer.emit (events.js:107:17)
    at REPLServer.Interface._onLine (readline.js:214:10)
    at REPLServer.Interface._line (readline.js:553:8)
    at REPLServer.Interface._ttyWrite (readline.js:830:14)
    at ReadStream.onkeypress (readline.js:109:10)

Digging a litter deeper:

> var r = require('requestretry').Request.request.defaults({pool: false});
undefined
> r
{ [Function]
  get: [Function],
  head: [Function],
  post: [Function],
  put: [Function],
  patch: [Function],
  del: [Function],
  cookie: [Function],
  jar: [Function],
  defaults: [Function] }

This is the wrapper around the request module as indicated in their documentation here. This isn't unexpected based on what is done here but it would be nice to be able to mimic this behavior in this module.

Some errors are retried until max but then returned as successful

I see a weird behaviour when using the retry mechanism.

I'm doing request to a server that sometimes response with 500, internal server error, and when it does, I usually get those 500 for all retried requests.

The problem I see is that when the retries are done, and even the last request failed, I get called in the then function and not catch. I have tried the callback based version as well, and even here the err is empty.

Any other error, like pulling the network cable is handled fine.

The config is as follows:

  request({
    method: 'POST',
    uri: url,
    body: params,
    auth: { username: username, password: password },
    json: true,
    timeout: 5000,
    maxAttempts: 5, // try 5 times
    retryDelay: 1000, // wait for 1s before trying again
    retryStrategy: request.RetryStrategies.HTTPOrNetworkError // (default) retry on 5xx or network errors
  }

And this is the response I get to the then function after last failed retry with sensitive data replaced with ***:

IncomingMessage {
  _readableState: 
   ReadableState {
     objectMode: false,
     highWaterMark: 16384,
     buffer: [],
     length: 0,
     pipes: null,
     pipesCount: 0,
     flowing: true,
     ended: true,
     endEmitted: true,
     reading: false,
     sync: false,
     needReadable: false,
     emittedReadable: false,
     readableListening: false,
     defaultEncoding: 'utf8',
     ranOut: false,
     awaitDrain: 0,
     readingMore: false,
     decoder: null,
     encoding: null,
     resumeScheduled: false },
  readable: false,
  domain: null,
  _events: 
   { end: [ [Function: responseOnEnd], [Function], [Function], [Function] ],
     close: [ [Function], [Function] ],
     data: [Function],
     error: [Function] },
  _eventsCount: 4,
  _maxListeners: undefined,
  socket: 
   TLSSocket {
     _tlsOptions: 
      { pipe: null,
        secureContext: [Object],
        isServer: false,
        requestCert: true,
        rejectUnauthorized: true,
        session: ***,
        NPNProtocols: undefined,
        ALPNProtocols: undefined,
        requestOCSP: undefined },
     _secureEstablished: true,
     _securePending: false,
     _newSessionPending: false,
     _controlReleased: true,
     _SNICallback: null,
     servername: null,
     npnProtocol: undefined,
     alpnProtocol: false,
     authorized: true,
     authorizationError: null,
     encrypted: true,
     _events: 
      { close: [Object],
        end: [Object],
        finish: [Function: onSocketFinish],
        _socketEnd: [Function: onSocketEnd],
        secure: [Function],
        free: [Function: onFree],
        agentRemove: [Function: onRemove],
        drain: [Function: ondrain],
        error: [Object],
        data: [Function: socketOnData],
        timeout: [Object] },
     _eventsCount: 11,
     _connecting: false,
     _hadError: false,
     _handle: null,
     _parent: null,
     _host: '***',
     _readableState: 
      ReadableState {
        objectMode: false,
        highWaterMark: 16384,
        buffer: [],
        length: 0,
        pipes: null,
        pipesCount: 0,
        flowing: true,
        ended: true,
        endEmitted: true,
        reading: false,
        sync: false,
        needReadable: false,
        emittedReadable: false,
        readableListening: false,
        defaultEncoding: 'utf8',
        ranOut: false,
        awaitDrain: 0,
        readingMore: false,
        decoder: null,
        encoding: null,
        resumeScheduled: false },
     readable: false,
     domain: null,
     _maxListeners: 0,
     _writableState: 
      WritableState {
        objectMode: false,
        highWaterMark: 16384,
        needDrain: false,
        ending: true,
        ended: true,
        finished: true,
        decodeStrings: false,
        defaultEncoding: 'utf8',
        length: 0,
        writing: false,
        corked: 0,
        sync: false,
        bufferProcessing: false,
        onwrite: [Function],
        writecb: null,
        writelen: 0,
        bufferedRequest: null,
        lastBufferedRequest: null,
        pendingcb: 0,
        prefinished: true,
        errorEmitted: false },
     writable: false,
     allowHalfOpen: false,
     destroyed: true,
     bytesRead: 346,
     _bytesDispatched: 375,
     _sockname: null,
     _pendingData: null,
     _pendingEncoding: '',
     ssl: 
      TLSWrap {
        _externalStream: {},
        fd: -22,
        _parent: [Object],
        _parentWrap: undefined,
        _secureContext: [Object],
        reading: true,
        owner: [Circular],
        onread: [Function: noop],
        writeQueueSize: 1,
        onhandshakestart: [Function],
        onhandshakedone: [Function: bound ],
        onocspresponse: [Function: bound onocspresponse],
        onerror: [Function] },
     server: undefined,
     _requestCert: true,
     _rejectUnauthorized: true,
     parser: null,
     _httpMessage: 
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 5,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedHeader: [Object],
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Circular],
        connection: [Circular],
        _header: '***',
        _headers: [Object],
        _headerNames: [Object],
        _onPendingData: null,
        agent: [Object],
        socketPath: undefined,
        method: 'POST',
        path: '***',
        timeoutCb: [Function: emitTimeout],
        parser: null,
        res: [Circular] },
     _idleTimeout: -1,
     _idleNext: null,
     _idlePrev: null,
     _idleStart: 28882,
     read: [Function],
     _consuming: true,
     write: [Function: writeAfterFIN] },
  connection: 
   TLSSocket {
     _tlsOptions: 
      { pipe: null,
        secureContext: [Object],
        isServer: false,
        requestCert: true,
        rejectUnauthorized: true,
        session: ***,
        NPNProtocols: undefined,
        ALPNProtocols: undefined,
        requestOCSP: undefined },
     _secureEstablished: true,
     _securePending: false,
     _newSessionPending: false,
     _controlReleased: true,
     _SNICallback: null,
     servername: null,
     npnProtocol: undefined,
     alpnProtocol: false,
     authorized: true,
     authorizationError: null,
     encrypted: true,
     _events: 
      { close: [Object],
        end: [Object],
        finish: [Function: onSocketFinish],
        _socketEnd: [Function: onSocketEnd],
        secure: [Function],
        free: [Function: onFree],
        agentRemove: [Function: onRemove],
        drain: [Function: ondrain],
        error: [Object],
        data: [Function: socketOnData],
        timeout: [Object] },
     _eventsCount: 11,
     _connecting: false,
     _hadError: false,
     _handle: null,
     _parent: null,
     _host: '***',
     _readableState: 
      ReadableState {
        objectMode: false,
        highWaterMark: 16384,
        buffer: [],
        length: 0,
        pipes: null,
        pipesCount: 0,
        flowing: true,
        ended: true,
        endEmitted: true,
        reading: false,
        sync: false,
        needReadable: false,
        emittedReadable: false,
        readableListening: false,
        defaultEncoding: 'utf8',
        ranOut: false,
        awaitDrain: 0,
        readingMore: false,
        decoder: null,
        encoding: null,
        resumeScheduled: false },
     readable: false,
     domain: null,
     _maxListeners: 0,
     _writableState: 
      WritableState {
        objectMode: false,
        highWaterMark: 16384,
        needDrain: false,
        ending: true,
        ended: true,
        finished: true,
        decodeStrings: false,
        defaultEncoding: 'utf8',
        length: 0,
        writing: false,
        corked: 0,
        sync: false,
        bufferProcessing: false,
        onwrite: [Function],
        writecb: null,
        writelen: 0,
        bufferedRequest: null,
        lastBufferedRequest: null,
        pendingcb: 0,
        prefinished: true,
        errorEmitted: false },
     writable: false,
     allowHalfOpen: false,
     destroyed: true,
     bytesRead: 346,
     _bytesDispatched: 375,
     _sockname: null,
     _pendingData: null,
     _pendingEncoding: '',
     ssl: 
      TLSWrap {
        _externalStream: {},
        fd: -22,
        _parent: [Object],
        _parentWrap: undefined,
        _secureContext: [Object],
        reading: true,
        owner: [Circular],
        onread: [Function: noop],
        writeQueueSize: 1,
        onhandshakestart: [Function],
        onhandshakedone: [Function: bound ],
        onocspresponse: [Function: bound onocspresponse],
        onerror: [Function] },
     server: undefined,
     _requestCert: true,
     _rejectUnauthorized: true,
     parser: null,
     _httpMessage: 
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 5,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedHeader: [Object],
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Circular],
        connection: [Circular],
        _header: '***',
        _headers: [Object],
        _headerNames: [Object],
        _onPendingData: null,
        agent: [Object],
        socketPath: undefined,
        method: 'POST',
        path: '***',
        timeoutCb: [Function: emitTimeout],
        parser: null,
        res: [Circular] },
     _idleTimeout: -1,
     _idleNext: null,
     _idlePrev: null,
     _idleStart: 28882,
     read: [Function],
     _consuming: true,
     write: [Function: writeAfterFIN] },
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  httpVersion: '1.1',
  complete: true,
  headers: 
   { 'content-type': 'text/xml',
     'x-backside-transport': 'FAIL FAIL',
     connection: 'close' },
  rawHeaders: 
   [ 'Content-Type',
     'text/xml',
     'X-Backside-Transport',
     'FAIL FAIL',
     'Connection',
     'close' ],
  trailers: {},
  rawTrailers: [],
  upgrade: false,
  url: '',
  method: null,
  statusCode: 500,
  statusMessage: 'Internal Server Error',
  client: 
   TLSSocket {
     _tlsOptions: 
      { pipe: null,
        secureContext: [Object],
        isServer: false,
        requestCert: true,
        rejectUnauthorized: true,
        session: ***,
        NPNProtocols: undefined,
        ALPNProtocols: undefined,
        requestOCSP: undefined },
     _secureEstablished: true,
     _securePending: false,
     _newSessionPending: false,
     _controlReleased: true,
     _SNICallback: null,
     servername: null,
     npnProtocol: undefined,
     alpnProtocol: false,
     authorized: true,
     authorizationError: null,
     encrypted: true,
     _events: 
      { close: [Object],
        end: [Object],
        finish: [Function: onSocketFinish],
        _socketEnd: [Function: onSocketEnd],
        secure: [Function],
        free: [Function: onFree],
        agentRemove: [Function: onRemove],
        drain: [Function: ondrain],
        error: [Object],
        data: [Function: socketOnData],
        timeout: [Object] },
     _eventsCount: 11,
     _connecting: false,
     _hadError: false,
     _handle: null,
     _parent: null,
     _host: '***',
     _readableState: 
      ReadableState {
        objectMode: false,
        highWaterMark: 16384,
        buffer: [],
        length: 0,
        pipes: null,
        pipesCount: 0,
        flowing: true,
        ended: true,
        endEmitted: true,
        reading: false,
        sync: false,
        needReadable: false,
        emittedReadable: false,
        readableListening: false,
        defaultEncoding: 'utf8',
        ranOut: false,
        awaitDrain: 0,
        readingMore: false,
        decoder: null,
        encoding: null,
        resumeScheduled: false },
     readable: false,
     domain: null,
     _maxListeners: 0,
     _writableState: 
      WritableState {
        objectMode: false,
        highWaterMark: 16384,
        needDrain: false,
        ending: true,
        ended: true,
        finished: true,
        decodeStrings: false,
        defaultEncoding: 'utf8',
        length: 0,
        writing: false,
        corked: 0,
        sync: false,
        bufferProcessing: false,
        onwrite: [Function],
        writecb: null,
        writelen: 0,
        bufferedRequest: null,
        lastBufferedRequest: null,
        pendingcb: 0,
        prefinished: true,
        errorEmitted: false },
     writable: false,
     allowHalfOpen: false,
     destroyed: true,
     bytesRead: 346,
     _bytesDispatched: 375,
     _sockname: null,
     _pendingData: null,
     _pendingEncoding: '',
     ssl: 
      TLSWrap {
        _externalStream: {},
        fd: -22,
        _parent: [Object],
        _parentWrap: undefined,
        _secureContext: [Object],
        reading: true,
        owner: [Circular],
        onread: [Function: noop],
        writeQueueSize: 1,
        onhandshakestart: [Function],
        onhandshakedone: [Function: bound ],
        onocspresponse: [Function: bound onocspresponse],
        onerror: [Function] },
     server: undefined,
     _requestCert: true,
     _rejectUnauthorized: true,
     parser: null,
     _httpMessage: 
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 5,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedHeader: [Object],
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Circular],
        connection: [Circular],
        _header: '***',
        _headers: [Object],
        _headerNames: [Object],
        _onPendingData: null,
        agent: [Object],
        socketPath: undefined,
        method: 'POST',
        path: '***',
        timeoutCb: [Function: emitTimeout],
        parser: null,
        res: [Circular] },
     _idleTimeout: -1,
     _idleNext: null,
     _idlePrev: null,
     _idleStart: 28882,
     read: [Function],
     _consuming: true,
     write: [Function: writeAfterFIN] },
  _consuming: true,
  _dumped: false,
  req: 
   ClientRequest {
     domain: null,
     _events: 
      { socket: [Object],
        timeout: [Object],
        response: [Function: bound ],
        error: [Function: bound ],
        drain: [Function] },
     _eventsCount: 5,
     _maxListeners: undefined,
     output: [],
     outputEncodings: [],
     outputCallbacks: [],
     outputSize: 0,
     writable: true,
     _last: true,
     chunkedEncoding: false,
     shouldKeepAlive: false,
     useChunkedEncodingByDefault: true,
     sendDate: false,
     _removedHeader: { 'content-length': false },
     _contentLength: null,
     _hasBody: true,
     _trailer: '',
     finished: true,
     _headerSent: true,
     socket: 
      TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: null,
        npnProtocol: undefined,
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object],
        _eventsCount: 11,
        _connecting: false,
        _hadError: false,
        _handle: null,
        _parent: null,
        _host: '***',
        _readableState: [Object],
        readable: false,
        domain: null,
        _maxListeners: 0,
        _writableState: [Object],
        writable: false,
        allowHalfOpen: false,
        destroyed: true,
        bytesRead: 346,
        _bytesDispatched: 375,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        ssl: [Object],
        server: undefined,
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        _idleTimeout: -1,
        _idleNext: null,
        _idlePrev: null,
        _idleStart: 28882,
        read: [Function],
        _consuming: true,
        write: [Function: writeAfterFIN] },
     connection: 
      TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: null,
        npnProtocol: undefined,
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object],
        _eventsCount: 11,
        _connecting: false,
        _hadError: false,
        _handle: null,
        _parent: null,
        _host: '***',
        _readableState: [Object],
        readable: false,
        domain: null,
        _maxListeners: 0,
        _writableState: [Object],
        writable: false,
        allowHalfOpen: false,
        destroyed: true,
        bytesRead: 346,
        _bytesDispatched: 375,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        ssl: [Object],
        server: undefined,
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        _idleTimeout: -1,
        _idleNext: null,
        _idlePrev: null,
        _idleStart: 28882,
        read: [Function],
        _consuming: true,
        write: [Function: writeAfterFIN] },
     _header: '***',
     _headers: 
      { host: '***',
        authorization: '***',
        accept: 'application/json',
        'content-type': 'application/json',
        'content-length': 120 },
     _headerNames: 
      { host: 'host',
        authorization: 'authorization',
        accept: 'accept',
        'content-type': 'content-type',
        'content-length': 'content-length' },
     _onPendingData: null,
     agent: 
      Agent {
        domain: null,
        _events: [Object],
        _eventsCount: 1,
        _maxListeners: undefined,
        defaultPort: 443,
        protocol: 'https:',
        options: [Object],
        requests: {},
        sockets: [Object],
        freeSockets: {},
        keepAliveMsecs: 1000,
        keepAlive: false,
        maxSockets: Infinity,
        maxFreeSockets: 256,
        maxCachedSessions: 100,
        _sessionCache: [Object] },
     socketPath: undefined,
     method: 'POST',
     path: '***',
     timeoutCb: [Function: emitTimeout],
     parser: null,
     res: [Circular] },
  request: 
   Request {
     domain: null,
     _events: 
      { error: [Function: bound ],
        complete: [Function: bound ],
        pipe: [Function],
        end: [Object],
        data: [Function] },
     _eventsCount: 5,
     _maxListeners: undefined,
     method: 'POST',
     uri: 
      Url {
        protocol: 'https:',
        slashes: true,
        auth: null,
        host: '***',
        port: 443,
        hostname: '***',
        hash: null,
        search: null,
        query: null,
        pathname: '***',
        path: '***',
        href: '***' },
     body: '***',
     timeout: 10000,
     maxAttempts: 5,
     retryDelay: 1000,
     retryStrategy: [Function: HTTPError],
     fullResponse: true,
     promiseFactory: [Function: defaultPromiseFactory],
     callback: [Function],
     readable: true,
     writable: true,
     explicitMethod: true,
     _qs: 
      Querystring {
        request: [Circular],
        lib: [Object],
        useQuerystring: undefined,
        parseOptions: {},
        stringifyOptions: {} },
     _auth: 
      Auth {
        request: [Circular],
        hasAuth: true,
        sentAuth: true,
        bearerToken: null,
        user: '***',
        pass: '***' },
     _oauth: OAuth { request: [Circular], params: null },
     _multipart: 
      Multipart {
        request: [Circular],
        boundary: '6b8fcb2b-95fb-4465-9682-a05ea8b32c25',
        chunked: false,
        body: null },
     _redirect: 
      Redirect {
        request: [Circular],
        followRedirect: true,
        followRedirects: true,
        followAllRedirects: false,
        allowRedirect: [Function],
        maxRedirects: 10,
        redirects: [],
        redirectsFollowed: 0,
        removeRefererHeader: false },
     _tunnel: 
      Tunnel {
        request: [Circular],
        proxyHeaderWhiteList: [Object],
        proxyHeaderExclusiveList: [] },
     headers: 
      { authorization: '***',
        accept: 'application/json',
        'content-type': 'application/json',
        'content-length': 120 },
     setHeader: [Function],
     hasHeader: [Function],
     getHeader: [Function],
     removeHeader: [Function],
     localAddress: undefined,
     pool: {},
     dests: [],
     __isRequestRequest: true,
     _callback: [Function: bound ],
     proxy: null,
     tunnel: true,
     setHost: true,
     originalCookieHeader: undefined,
     _disableCookies: true,
     _jar: undefined,
     port: 443,
     host: '***',
     path: '***',
     _json: true,
     httpModule: 
      { Server: [Object],
        createServer: [Function],
        globalAgent: [Object],
        Agent: [Object],
        request: [Function],
        get: [Function] },
     agentClass: { [Function: Agent] super_: [Object] },
     agent: 
      Agent {
        domain: null,
        _events: [Object],
        _eventsCount: 1,
        _maxListeners: undefined,
        defaultPort: 443,
        protocol: 'https:',
        options: [Object],
        requests: {},
        sockets: [Object],
        freeSockets: {},
        keepAliveMsecs: 1000,
        keepAlive: false,
        maxSockets: Infinity,
        maxFreeSockets: 256,
        maxCachedSessions: 100,
        _sessionCache: [Object] },
     _started: true,
     href: '***',
     req: 
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 5,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedHeader: [Object],
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Object],
        connection: [Object],
        _header: '***',
        _headers: [Object],
        _headerNames: [Object],
        _onPendingData: null,
        agent: [Object],
        socketPath: undefined,
        method: 'POST',
        path: '***',
        timeoutCb: [Function: emitTimeout],
        parser: null,
        res: [Circular] },
     timeoutTimer: null,
     ntick: true,
     response: [Circular],
     originalHost: '***',
     originalHostHeaderName: 'host',
     responseContent: [Circular],
     _destdata: true,
     _ended: true,
     _callbackCalled: true },
  toJSON: [Function: responseToJSON],
  caseless: 
   Caseless {
     dict: 
      { 'content-type': 'text/xml',
        'x-backside-transport': 'FAIL FAIL',
        connection: 'close' } },
  read: [Function],
  body: '<?xml version=\'1.0\' ?>\n<env:Envelope xmlns:env=\'http://schemas.xmlsoap.org/soap/envelope/\'>\n<env:Body>\n<env:Fault>\n<faultcode>env:Client</faultcode>\n<faultstring>Internal Error</faultstring>\n</env:Fault>\n</env:Body>\n</env:Envelope>\n',
  attempts: 5 }

Any ideas?

TypeError: Object #<Request> has no method 'setMaxListeners'

In my code there's a part that requires me to do setMaxListeners

var request = require('requestretry');
var req = request (options);
req.setMaxListeners(0);

Above code will throw an error

TypeError: Object #<Request> has no method 'setMaxListeners'

This used to work with default request object, so as a drop-in plugin this probably will require code refactoring.

Feature request: get failure stats in callback

It would be nice to be able to get some sort of failure stats in the callback as we currently don't get any feedback on how many retries were made etc.

Excellent wrapper for request, though! Suits our needs almost exactly.

Support url change in retryStrategy

Hi,

I am wondering if there is a way to change the url on a retry using a custom retryStrategy. For instance, if I wanted to ping a server https://somesite.com/id/{5-digit-id} and I was creating a random 5-digit integer, trying it and if it failed, I would want to generate another 5-digit integer and try that one, is there a way to change the url like that in between retries?

Thank you very much

proxy all promise helpers

Rather than proxy 'then', 'catch', 'finally', 'fail', and 'done' to the underlying promise returned by promiseFactory, it would be nice if the whole api of the promise was exposed. For instance, if I use Q I would like to call .tap or .get on the return value.

Use with request options...

Hi!
I'm trying to use node-request-retry.
I need to use options parameter for request, since I need to use a proxy.
Is it possible to pass requestretry an 'options' object parameter AND options for requestretry (maxAttempts, retryDelay, retryStrategy) ? If yes, how?
TIA!

Usage with request-promise-native

Hi,

I'm using request-promise-native in order to be able to use native ES6 promises and i'm trying to figure out what's the ideal way to use your lib with it.

Iv'e build a custom component for my requests, for example:

const requestPromise = require('request-promise-native');

post(path = null, body = {}, headers = {}) {
	return requestPromise({
		method: 'POST',
		url: setBaseApiUrl(path),
		headers: setHeaders(headers),
		body: body,
		json: true,
		resolveWithFullResponse: true
	});
}

I'd like to keep this component as clean as possible, should the retry strategy be implemented whre post is being called (several locations and by several components)?
Also, not sure if using the node-request-retry wrapper would override request-promise-native in some way.
I would love it if you could point me in the right direction.

Thanks.

Request callback not called somehow

Just a very normal get request, but the callback was never called, after changing the request to original request, the callback was called correctly.

request.get(reqOptions, function (err, response, body) {});

This caused a serious production issue, not just one requests that accidentally have this kind of behavior, I have sent about hundreds of requests, none of them returned.

It did work at first, it's still working on UAT environment, but there must be a glitch that could lead to this.

request.jar is not a function

Hello, i just tried replacing the require('request') with requestretry but looks like there is no support for request.jar.

Also i wounder if the request options.jar is supported.

Thanks.

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/39128571-request-jar-is-not-a-function?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github).

backoff strategy, expose attempts on error, event on each retry

Consider to add:

  • Fibonacci, exponential, fixed and linear backoffs
  • event on each retry
  • expose attempts on error
request({
            method: 'POST',
            json: true,
            maxAttempts: 10,
            retryDelay: 5000,
            retryStrategy: request.RetryStrategies.HTTPOrNetworkError
        }).then((response) => {
            log.info(`attempts: ${response.attempts}`); // attempts available here
        }).catch((error) => {
            log.error(`failed ${error.message}`); // attempts NOT available here
        });

Setting request options for the request module

What is the simplest way to muck about in the request module?

Specifically, I'd like to set global options for things like the pool, method, etc.

Doing this:

var request = require('requestretry').defaults({my: options})

Results in the following:

var request = require('requestretry').defaults({pool:false});
                                       ^
TypeError: undefined is not a function

Any assistance would be greatly appreciated.

Retry even if any error was thrown

Would be nice if retries could be scheduled even if there's no err object.
There are some API's that never return something different than HTTP 200 and the errors are wrapped inside the body, in this case the retryStrategy is never called.

Impossible to get number of retries in delayStrategy when connection is refused

Hi,
I implemented an exponential backoff delay strategy. In order for that to work, I access response.attempts.
The problem is that when an ECONNREFUSED error occurs, the arguments for the strategy are just strategy(connectionRefusedError, undefined, undefined).

  if (!response) { // connection completely refused
    console.error('cannot apply exponential backoff when there is no response', err && err.message)
    return DOWNLOAD_BASE_DELAY
  }
  const currentAttempt = response.attempts - 1 // begins with 1
  const delay = Math.floor(Math.pow(2, currentAttempt) * DOWNLOAD_BASE_DELAY)
  log('using exponential backoff to wait %s ms for the retry', delay, currentAttempt, response.request.uri)
  return delay
}

const request = requestretry.defaults({
  maxAttemps: DOWNLOAD_MAX_RETRIES,
  delayStrategy: exponentialBackoff
})

I suggest an additional argument to the delayStrategy function containing an object with meta information, like

  maxAttemps: 5,
  attempt: 2,
  requestURL: 'http://example.com'
}

This would support stateful delay strategies and be future proof.

PS: a workaround is to count attempts in a closure variable, but this is not possible in my scenario using .defaults({ delayStrategy: ...}) and is pretty ugly.


Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

how to return 400 errors with error code not response code?

            app.use((err, req, res, next) => {
                console.log('err',err)
                console.error('err.statusCode',err.statusCode);
                console.error('err.error',err.error);
                console.error('err.stack',err.stack);
                res.status(err.statusCode || 500).send(err.error || 'Internal server error.');
            });

I have error handling function as from above, where 400 errors can only be logged with res.statusCode with the module, but is there any way to customize this module, make retries for 500 errors and still consider 400s as errors with err.statusCode?

Should make it clear in the document that separate url parameter is not supported

It turned out that request(url, opts) type of call is not supported in this module, instead we have to use request(opts) with url as a field in the opts object. My requests were failing because of this and it wasted me a couple of hours to figure out. Document says this is a drop in replacement of the original request module, it is not, please make it clear in the doc, thanks.

Always uses an in-memory buffer, never allows streaming to disk

I wanted to try using this module instead of request because it would handle ESOCKETTIMEDOUT (and more) errors for me. However, there is a big issue when it comes to streaming.

The bl module uses Node's buffer.js. Node buffers have a limit of 256MB. I get the error

Uncaught Exception:
Error: "toString()" failed
...

when I use this module, even though only pass in the options object as an argument to requestretry.

Here is the culprit in your code. I may fork and fix the code myself, but I am not sure if I have enough time to think of all the things I might break for other users of this module.

this._req = Request.request(this.options, function (err, response, body) {
    if (response) {
      response.attempts = this.attempts;
    }
    if (this.retryStrategy(err, response, body) && this.maxAttempts > 0) {
      this._timeout = setTimeout(this._tryUntilFail.bind(this), this.retryDelay);
      return;
    }

    this.reply(err, response, body);
  }.bind(this));

A solution, I think, is to listen to the .on( 'end' ... event instead of passing in the callback to request. Of course, sending the callback is fine IF AND ONLY IF the user passes a callback in as well. The callback destroys the opportunity we have to use the excellent streaming interface on the request module.

When downloading large files (and in my case, multiple large files. Perhaps several over 1GB), this module is impossible to use. Once we pass a callback to request, we are telling request to buffer data in memory. By using the streaming interface, we are able to download several huge files because the data goes in and out of memory (drains) as fast as possible.

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/37570683-always-uses-an-in-memory-buffer-never-allows-streaming-to-disk?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github).

retry on http 408 error ?

Hi !
Depending on load, we might see some http 408 error (Request Time-out) and I think it's a good code to retry, since it means either the client was to slow to send the request, or the server was too busy to receive it.

What do you think ?

fg-lodash depends on a deprecated version of lodash

The dependency fg-lodash depends on a deprecated version of lodash. I've made a pull request to fg-lodash to update to lodash v4.0.0, but it may be better to remove this dependency and replace it with standard lodash. The version of lodash upon which fg-lodash depends is very outdated.

retry logging method

Hi

I need to log every time request does a retry, but I don't see any way to execute a function between retries... Is there a way to do it? If not, is this something you can implement for future versions ?

Regards

Is there a way to see attempts during a string of failures when using Promises?

I see that for the callback style code but it looks like .then will only be processed on first success and .catch will only be called on final failure. I'd like to be able to log failures as they happen when using promises. I guess there's no way to do that though. None that I can see in the code. I was thinking an event emitter?

This would be helpful for watching failures in the logs without using callbacks.

How to listen to finish of a write stream if requests were retried?

I have:

    return new Promise<string>(resolve => {
        request(options)
            .pipe(fs.createWriteStream(fileName))
            .on("finish", function () {
                console.log("The file should be completely written here");
                resolve(fileName);
            });
    });

and I expected the finish to only be emitted if one of my retryable requests went through.

Instead, if there is a retry, the finish will be emitted already for the first request and resolve the promise prematurely.

I want to resolve my Promise if and only if one request was successful and the writing to the stream finished with the content. How do I achieve that?

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/38111507-how-to-listen-to-finish-of-a-write-stream-if-requests-were-retried?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github).

Service returning 503 errors doesn't have an error object

I'm using a custom delayStrategy and which uses the number of attempts in the error object to compute the new time but in the case of 503 error it doesn't have an error object so I get:

TypeError: Cannot read property 'attempts' of null
  at Request.customDelayStrategy [as delayStrategy]

Feature request: Retry strategy for response header

I am using retry this way:

const fs = requrie("fs");
const request = require("requestretry");

export class FileFetcher {
    public async fetchFile(url: string): Promise<string> {
        const fileName = "./download.zip";
        return new Promise<string>(resolve => {
            request(url).pipe()
                .pipe(fs.createWriteStream(fileName))
                .on("close", function () {
                    resolve(fileName);
                });
        });
    }
}

I was wondering why the retry mechanism would not work and that I always got an empty file.

I realized it was due to a badly behaving external api, that will always return 200 OK, even if the ressource I want to fetch is not yet available.

When I request it via httpie:

$ http http://evil-api.foo/download.zip

the response is without an existing file would be:

HTTP/1.1 200 OK
Cache-Control: private, max-age=43200
Connection: keep-alive
Content-Length: 0
Date: Thu, 29 Sep 2016 15:26:40 GMT
Server: nginx
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET

Response if it exists:

HTTP/1.1 200 OK
Cache-Control: private, max-age=43200
Connection: keep-alive
Content-Length: 33989
Content-Type: application/x-zip-compressed
Date: Thu, 29 Sep 2016 14:51:42 GMT
Server: nginx
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
+-----------------------------------------+
| NOTE: binary data not shown in terminal |
+-----------------------------------------+

Hence, I am wondering if you could please add a retry strategy that checks for certain response header. In my case, I want to retry for 10 times as long as Content-Length: 0.

request from defaults() method ignores options argument in some cases.

request from defaults() method ignores options argument in some cases.

var request = require('requestretry').defaults({
    headers: {
        'User-Agent': 'default UA'
    }
});

// OK
request('https://github.com/', function (err, response, body) {
});

// NOT OK - Referer header is not sent
request('https://github.com/', {
    headers: {
        Referer: 'https://www.apple.com/'
    }
}, function (err, response, body) {
});

// OK
request({
    url: 'https://github.com/',
    headers: {
        Referer: 'https://www.apple.com/'
    }
}, function (err, response, body) {
});

// OK
request.get('https://github.com/', {
    headers: {
        Referer: 'https://www.apple.com/'
    }
}, function (err, response, body) {
});

Merge into core request module

This package extends the request module with retry functionality. Have maintainers considered merging this functionality into the main request module?

Failed to minify the code in strategies

Using webpack on a React, ES, Babel site. "requestretry": "^3.0.0",

Failed to minify the code from this file: 

 	./node_modules/requestretry/strategies/HTTPError.js:9 

Read more here: http://bit.ly/2tRViJ9

`request.get(...).auth()` is broken

In the request documentation, it's said that one can do request.get(...).auth(user, pass) for basic authentication. Nevertheless, this is broken in requestretry.

Should it have the same API as request or am I missing something?

--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/40581074-request-get-auth-is-broken?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F1861872&utm_medium=issues&utm_source=github).

Are retry requests closed properly?

After using requestretry for 500 of connections over 10 hours, it will reach Error: getaddrinfo ENOTFOUND. I wonder if it is due to connections not closed.

Check attempts on error

Shouldn't there be a way to clock how many times the request was attempted in the case of an error (not just for bad responses)?

function responseHandler(error, response, body) {
  if (error) console.log(error.attempts);
  if (response) console.log(response.attempts);
}

How to combine node-request-retry with node-delayed-request ?

I need to combine node-delayed-request (https://github.com/leeroybrun/node-delayed-request) module with node-request-retry (this module), since I need to place delayed requests, and to be able to retry the request in case it fail.
Which strategy would you suggest ?
Should I fork this module, changing the dependency from 'request' to 'delayed-request' ? Do you think this could possibly work? Or should I better do the reverse, forking 'delayed-request' changing the dependency from 'request' to 'request-retry' ?
Or should I make an entirely new module, manually integrating the two ?
Otherwise, do you have some cleverer alternative idea?
Thanks in advance!

ECONNRESET does not trigger requestRetry strategy (if behind a proxy)

I am using node-request-retry through a (TOR) proxy.

In the node-request-retry README I read:

When the connection fails with one of ECONNRESET, ENOTFOUND, ESOCKETTIMEDOUT, ETIMEDOUT, ECONNREFUSED, EHOSTUNREACH, EPIPE, EAI_AGAIN or when an HTTP 5xx error occurs, the request will automatically be re-attempted as these are often recoverable errors and will go away on retry.

Sometimes, though (very few times indeed) the request errors out (no retry strategy entered) with this request error:

[Error: socket hang up] code: 'ECONNRESET'

Is this a bug in node-request-retry? Or possibly the sentence "the request will automatically be re-attempted" means that the retry is handled internally (without retry strategies), and after a number (which number?) of attempts, it errors out to the caller?

Support for streams

Hello,

I'm trying to use node-request-retry but I hit a issue and I can't figure out a proper workaround:

Using a writeable stream to write a remote file on disc, if the first GET fails, node-request-retry will retry the request; but only that first request will be sent to the pipe (and trigger the response event). The subsequent requests (successful or not) won't trigger those event.
As a result, I end up with a incorrect file (the result of the failed GET), even if the GET did succeed on the second try and returned the correct file.

Is there a way to make that flow work properly?

Thanks!

Add Proxy to Script

Our data scraping project used node-request-retry npm modules for data requesting and scraping.

According to the project cope we have use the Proxy server to scrape the data from the third party site. We have tried different scenarios to achieve this target but I was not successful. Is there a way to use this script with proxy server? or any examples please pass us and the community.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.