Retry Plugin
Automatically retry failed oRPC requests with customizable strategies controlled through client context options.
Setup
import { RetryLinkPlugin, RetryLinkPluginContext } from '@orpc/client/plugins'
interface ClientContext extends RetryLinkPluginContext {}
const link = new RPCLink<ClientContext>({
plugins: [
new RetryLinkPlugin(),
],
})
Usage
By default, retries are disabled. To enable retries, set the retry count in the request context:
const const planets: {
id: number;
name: string;
description?: string | undefined;
}[]
planets = await const client: {
planet: {
list: ProcedureClient<RetryLinkPluginContext, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, Record<never, never>>;
find: ProcedureClient<RetryLinkPluginContext, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<...>, Record<...>>;
create: ProcedureClient<...>;
};
}
client.planet: {
list: ProcedureClient<RetryLinkPluginContext, ZodObject<{
limit: ZodOptional<ZodNumber>;
cursor: ZodDefault<ZodNumber>;
}, $strip>, ZodArray<ZodObject<{
id: ZodNumber;
name: ZodString;
description: ZodOptional<ZodString>;
}, $strip>>, Record<never, never>>;
find: ProcedureClient<RetryLinkPluginContext, ZodObject<{
id: ZodNumber;
}, $strip>, ZodObject<...>, Record<...>>;
create: ProcedureClient<...>;
}
planet.list: Client
(input: {
limit?: number | undefined;
cursor?: number | undefined;
}, options?: FriendlyClientOptions<RetryLinkPluginContext> | undefined) => PromiseWithError<{
id: number;
name: string;
description?: string | undefined;
}[], Error>
list({ limit?: number | undefinedlimit: 10 }, {
context?: RetryLinkPluginContext | undefinedcontext: {
RetryLinkPluginContext.retry?: Value<Promisable<number>, [Omit<StandardLinkInterceptorOptions<RetryLinkPluginContext>, "next">]> | undefinedMaximum retry attempts before throwing.
Use `Number.POSITIVE_INFINITY` for infinite retries (e.g. for AsyncIteratorObject).retry: 3, // Maximum retry attempts
RetryLinkPluginContext.retryDelay?: Value<Promisable<number>, [RetryLinkPluginAttemptOptions<RetryLinkPluginContext>]> | undefinedDelay (in ms) before retrying.retryDelay: 2000, // Delay between retries in ms
RetryLinkPluginContext.shouldRetry?: Value<Promisable<boolean>, [RetryLinkPluginAttemptOptions<RetryLinkPluginContext>]> | undefinedDetermine whether to retry.shouldRetry: options: RetryLinkPluginAttemptOptions<RetryLinkPluginContext>options => true, // Determines whether to retry based on the error
RetryLinkPluginContext.onRetry?: ((options: RetryLinkPluginAttemptOptions<RetryLinkPluginContext>) => void | ((isSuccess: boolean) => void)) | undefinedHook called before each retry. Can return a cleanup callback.onRetry: (options: RetryLinkPluginAttemptOptions<RetryLinkPluginContext>options) => {
// Hook executed on each retry
return (isSuccess: booleanisSuccess) => {
// Execute after the retry is complete
}
},
}
})
Client Error Responses
By default, errors from a 4xx response other than 408 and 429 are not retried, since retrying would most likely fail the same way. To retry every error, set shouldRetry to true, or extend the default with isRetryableError:
import { ORPCError } from '@orpc/client'
import { isRetryableError } from '@orpc/client/plugins'
const planets = await client.planet.list({ limit: 10 }, {
context: {
retry: 3,
shouldRetry: ({ error }) => isRetryableError(error) || (error instanceof ORPCError && error.code === 'CONFLICT'),
}
})
Streamed Input
A retry sends the same input again. This is fine for plain values, Blob, and File, but streamed input, such as an AsyncIteratorObject or a ReadableStream<Uint8Array>, can be read only once. By the time a retry happens, the first attempt has already consumed part or all of the stream, so the retry would send incomplete input.
Avoid retrying calls with streamed input. If you enable retries by default, disable them based on the input:
import { isAsyncIteratorObject } from '@orpc/shared'
function isStreamedInput(input: unknown): boolean {
return input instanceof ReadableStream || isAsyncIteratorObject(input)
}
const link = new RPCLink<ClientContext>({
plugins: [
new RetryLinkPlugin({
default: {
retry: ({ input }) => isStreamedInput(input) ? 0 : 3,
},
}),
],
})
Event Source Simulation
To replicate the behavior of EventSource for an AsyncIteratorObject, use the following configuration:
const streaming = await client.streaming('the input', {
context: {
retry: Number.POSITIVE_INFINITY,
}
})
for await (const message of streaming) {
console.log(message)
}
Learn More
For implementation details, see the source code.