nice-grpc Client Retry Middleware
nice-grpc-client-middleware-retry is a client-side middleware for the `nice-grpc` TypeScript gRPC library, enabling automatic retries for unary gRPC calls. It integrates seamlessly with the `nice-grpc` client factory to add robust fault tolerance with features like exponential backoff. The current stable version is 3.1.14, and the `nice-grpc` ecosystem, which this package is part of, maintains an active and frequent release cadence, often with updates every few weeks for core packages. A key differentiator is its strong TypeScript support and modern API leveraging Promises and Async Iterables, providing a more ergonomic experience compared to traditional gRPC implementations. It also emphasizes the importance of idempotency, requiring explicit configuration for retries on non-idempotent operations to prevent unintended side effects.
Common errors
-
TypeError: __webpack_require__(...).retryMiddleware is not a function
cause Attempting to import `retryMiddleware` using CommonJS `require` syntax in an ES Module context, or an incorrect import path.fixUpdate your import statement to `import { retryMiddleware } from 'nice-grpc-client-middleware-retry';` and ensure your project is configured for ES Modules (e.g., `"type": "module"` in `package.json`). -
ClientError: status=UNKNOWN, details='Call cancelled'
cause The gRPC call was cancelled, often due to a client-side timeout expiring before the server responded, or a manual cancellation signal.fixIncrease the `callTimeout` in your gRPC call options or ensure that server processing times are within acceptable limits. Verify that `AbortSignal`s are not prematurely aborting the call. If retries are configured, ensure the total time for retries does not exceed an overarching call timeout. -
Error: TS2307: Cannot find module 'nice-grpc' or its corresponding type declarations.
cause TypeScript compiler cannot resolve the `nice-grpc` package or its types, typically due to missing `@types` packages or incorrect module resolution settings.fixEnsure `nice-grpc` (and its types, which it ships) is correctly installed: `npm install nice-grpc`. Check `tsconfig.json` for `"moduleResolution": "Node16"` or `"Bundler"` and `"module": "Node16"` or `"ESNext"` to correctly resolve ES Modules.
Warnings
- gotcha Retrying non-idempotent operations can lead to unintended side effects or data corruption if the original call succeeded but the client didn't receive the response. By default, `nice-grpc-client-middleware-retry` disables retries unless the gRPC method is explicitly marked as `idempotency_level = IDEMPOTENT` or `NO_SIDE_EFFECTS` in the Protobuf definition (requires `ts-proto` for option interpretation), or `retry: true` is set in call options.
- breaking Older versions of `nice-grpc` and its middlewares might have CJS-only exports. Since `nice-grpc` v3, the library primarily targets ESM, which can lead to import errors if your project is not configured for ESM or if you try to use `require()` statements.
- gotcha Configuring `retryableStatuses` without careful consideration can lead to retries on errors that are not transient, such as `INVALID_ARGUMENT` or `NOT_FOUND`. This can mask actual application logic errors and exhaust server resources.
- gotcha Infinite retries or very long retry configurations can lead to resource exhaustion on the client and put undue pressure on struggling backend services, potentially worsening an outage (the 'thundering herd' problem).
Install
-
npm install nice-grpc-client-middleware-retry -
yarn add nice-grpc-client-middleware-retry -
pnpm add nice-grpc-client-middleware-retry
Imports
- retryMiddleware
const { retryMiddleware } = require('nice-grpc-client-middleware-retry');import { retryMiddleware } from 'nice-grpc-client-middleware-retry'; - createClientFactory
import { createClientFactory } from 'nice-grpc-client-middleware-retry';import { createClientFactory } from 'nice-grpc'; - Status
import { Status } from '@grpc/grpc-js';import { Status } from 'nice-grpc';
Quickstart
import { createChannel, createClientFactory, ClientError, Status } from 'nice-grpc';
import { retryMiddleware } from 'nice-grpc-client-middleware-retry';
// Imagine you have a generated Protobuf service definition (e.g., from ts-proto)
// interface ExampleService extends Client<typeof ExampleServiceDefinition> {}
// const ExampleServiceDefinition = { ... }; // Your service definition
// Mock a service definition for demonstration
const ExampleServiceDefinition = {
name: 'ExampleService',
methods: {
exampleMethod: {
path: '/ExampleService/ExampleMethod',
requestType: {}, // replace with your actual request type
responseType: {}, // replace with your actual response type
options: {},
},
},
};
async function runClientWithRetries() {
const address = process.env.GRPC_SERVER_ADDRESS ?? 'localhost:50051';
const channel = createChannel(address);
const clientFactory = createClientFactory().use(retryMiddleware);
const client = clientFactory.create(ExampleServiceDefinition, channel);
let attempt = 0;
try {
console.log('Attempting to call exampleMethod with retries...');
const response = await client.exampleMethod({}, {
// Enable retry if method is not marked as idempotent in Protobuf options
retry: true,
retryMaxAttempts: 3,
retryableStatuses: [Status.UNAVAILABLE, Status.INTERNAL, Status.UNKNOWN],
onRetry(error: ClientError, attemptNumber: number) {
console.warn(`Retry attempt ${attemptNumber} due to ${Status[error.code]}: ${error.details}`);
},
// Optional: Exponential backoff configuration
retryDelay: 100, // initial delay in ms
retryDelayMax: 1000, // max delay in ms
retryDelayMultiplier: 2, // multiplier for each successive delay
});
console.log('Successfully received response:', response);
} catch (error) {
if (error instanceof ClientError) {
console.error(`Client error after all retries: ${Status[error.code]} - ${error.details}`);
} else {
console.error('An unexpected error occurred:', error);
}
} finally {
channel.close();
}
}
runClientWithRetries();