Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry mechanism #16

Closed
dalwlad opened this issue Feb 6, 2018 · 5 comments
Closed

Retry mechanism #16

dalwlad opened this issue Feb 6, 2018 · 5 comments
Labels

Comments

@dalwlad
Copy link

dalwlad commented Feb 6, 2018

I've just started using wretch and noticed the nice feature that allows adding custom middlewares for implementing cross-cutting concerns like caching, as you've nicely illustrated in the docs. Many times, there is a need for adding support for retry mechanism. Would it be possible to illustrate in a sample how a request retry middleware would look like?

@elbywan
Copy link
Owner

elbywan commented Feb 6, 2018

Hi @dalwlad !

You can indeed do that using a middleware, but if you need to retry a request on 401/403 (or any other error) you can also use catchers which should be easier.
I wrote an example of doing this in the following issue or in the docs.

And if you still need to use middlewares for this, I can post a code sample later today or tomorrow !

@dalwlad
Copy link
Author

dalwlad commented Feb 6, 2018

Thanks for the quick reply @elbywan. I've seen the example for how to use the catchers for this and they work just fine. But if you would need to implement the retry mechanism for any call that is hitting the api, that would become tedious. The use case I'm working right now has a requirement like : for all outgoing requests, there should be a retry mechanism, ideally indefinite, but the delay should be detemined by some sort of exponential function, with capping values: for example start at 100ms and increase it, but no more than 1000ms. In other words, you receive an error for a request, wait 100ms and try again, Next time, if you still receive the error, increase the timeout and try again ... you got the idea.

I was trying something like below, but got stuck on the resolve part where for the second attempt, it fails because the wretch call doesn't have any rejection handling.

const DEFAULTS = {
  ERROR_CODES : []
};

const reconnectMiddleware = ({
  errorCodes = DEFAULTS.ERROR_CODES
} = {}) => {
  const tracker = new Map();

  errorCodes.map(code => {
    tracker.set(code, new Map());
  });

  return next => (url, options) => {
    const req = next(url, options);

    req.then(response => {
      if (tracker.has(response.status)) {
        if (!response.ok) {
          const requestKey = `${options.method}@${url}`;
          const numberOfAttemptsMade = tracker.get(response.status).get(requestKey) || 0;

          tracker.get(response.status).set(requestKey, numberOfAttemptsMade + 1);

          return new Promise(resolve => {
            const delay = Math.max(DEFAULTS.DELAY * numberOfAttemptsMade, 1000);

            setTimeout(() => {
              resolve(wretch(url)[options.method.toLowerCase()](options));
            }, delay);
          });
        }

        tracker.set(response.status, new Map());
      }

      return response;
    });

    req.catch(response => {
      const requestKey = `${options.method}@${url}`;
      const numberOfAttemptsMade = tracker.get(response.status).get(requestKey) || 0;

      tracker.get(response.status).set(requestKey, numberOfAttemptsMade + 1);

      return new Promise(resolve => {
        const delay = Math.max(DEFAULTS.DELAY * numberOfAttemptsMade, 1000);

        setTimeout(() => {
          resolve(wretch(url)[options.method.toLowerCase()](options));
        }, delay);
      });
    });

    return req;
  };
};

export default reconnectMiddleware;

@elbywan
Copy link
Owner

elbywan commented Feb 7, 2018

@dalwlad Just made some corrections to your snippet, can you check with the code below ? I'm pretty sure it should work 😉 .

const DEFAULTS = {
  DELAY: 100,
  MAX_ATTEMPTS: 10,
  ERROR_CODES : []
};

const reconnectMiddleware = ({
  errorCodes = DEFAULTS.ERROR_CODES
} = {}) => {
  const tracker = new Map();

  errorCodes.map(code => {
    tracker.set(code, new Map());
  });

  return next => (url, options) => {
    const checkStatus = (response) => {
        if (tracker.has(response.status)) {
            if (!response.ok) {
                const requestKey = `${options.method}@${url}`;
                const numberOfAttemptsMade = tracker.get(response.status).get(requestKey) || 0;
                tracker.get(response.status).set(requestKey, numberOfAttemptsMade + 1);

                // Maybe add a maximum number of attempts ?
                if(numberOfAttemptsMade >= MAX_ATTEMPTS) 
                    return response

                // We need to recurse until we have a correct response and chain the checks
                return new Promise(resolve => {
                    const delay = Math.min(DEFAULTS.DELAY * numberOfAttemptsMade, 1000);
                    setTimeout(() => {
                        resolve(next(url, options));
                    }, delay);
                }).then(checkStatus);
            }

            // If ok - reset the map and return the response
            tracker.set(response.status, new Map());
            return Promise.resolve(response);
        }
        return response
    }
    // Willingly omitted .catch which prevents handling network errors and should throw
    return next(url, options).then(checkStatus);
  };
};

@dalwlad
Copy link
Author

dalwlad commented Feb 7, 2018

@elbywan Thanks for that. It works indeed!

@elbywan
Copy link
Owner

elbywan commented Feb 7, 2018

Great ! Closing the issue then !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants