connectrpc / connect-es

The TypeScript implementation of Connect: Protobuf RPC that works.
https://connectrpc.com/
Apache License 2.0
1.37k stars 79 forks source link

Client middleware that adds automatic retries to unary calls with timeouts between retry attempts. #987

Closed BrRenat closed 9 months ago

BrRenat commented 9 months ago

Is your feature request related to a problem? Please describe. I'm frustrated with intermittent network issues causing failures in unary calls.

Describe the solution you'd like Add automatic retry support with configurable timeouts to connect-es client middleware for unary calls.

Describe alternatives you've considered Manual retry logic at the application level introduces complexity and duplication. Centralizing retries in the middleware ensures consistency and ease of maintenance.

srikrsna-buf commented 9 months ago

Hey! Can you elaborate a bit more on unary calls failing, is it happening in the browser?

BrRenat commented 9 months ago

Yes, certain errors can be anticipated and retried, while others may not. The middleware should be configurable based on error codes. f.e network error or rate limit of api.

srikrsna-buf commented 9 months ago

Including the middleware as part of the core package seems out of scope. The logic to when and how to retry is beyond the scope of an RPC library, it purely depends on the environment and the API being accessed. But implementing a unary retry is straightforward:

import {
  Code,
  ConnectError,
  type Interceptor,
} from "@connectrpc/connect";

const retryInterceptor: Interceptor = (next) => {
  return async (req) => {
    if (req.stream) {
      return await next(req);
    }
    for (let i = 0; ; i++) {
      try {
        return await next(req);
      } catch (err) {
        if (i == 5) {
          throw err;
        }
        const cErr = ConnectError.from(err);
        if (cErr.code == Code.ResourceExhausted) {
          // Wait for a bit and retry
          await new Promise((resolve) => setTimeout(resolve, 1000));
        } else {
          throw err;
        }
      }
    }
  };
};

As you can see most of the code is just to decide and when and how to retry. Closing this issue for now, feel free to reopen if needed.