From f481c76d9b18b1b1f519ffea4795a463e1444dbd Mon Sep 17 00:00:00 2001 From: ivanauth Date: Tue, 16 Jun 2026 20:11:08 -0400 Subject: [PATCH 1/4] fix: disable gRPC retries by default to prevent unrecoverable failures --- src/v1.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/v1.ts b/src/v1.ts index 3fe76f8..f0677d9 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -75,6 +75,7 @@ class ZedClient implements ProxyHandler { ) { this.options = { ...options, + "grpc.enable_retries": 0, interceptors: [ ...(options?.interceptors ?? []), From 35ff81d409b3f4ecd158948197b73d1f89389ae4 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Tue, 16 Jun 2026 23:11:34 -0400 Subject: [PATCH 2/4] fix: make disabled retries overridable and apply to preconnected clients (#200) Move grpc.enable_retries before ...options so callers can re-enable retries, and pass this.options (which carries the setting) to the preconnected clients instead of the raw options, so the default actually applies to the default PermissionsService client. --- src/v1.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/v1.ts b/src/v1.ts index f0677d9..05ae786 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -74,8 +74,11 @@ class ZedClient implements ProxyHandler { options: grpc.ClientOptions | undefined, ) { this.options = { - ...options, + // Default to disabling gRPC retries to avoid unrecoverable failures + // through envoy/istio (see #112). Placed before ...options so callers can + // re-enable retries by passing "grpc.enable_retries" explicitly. "grpc.enable_retries": 0, + ...options, interceptors: [ ...(options?.interceptors ?? []), @@ -86,26 +89,26 @@ class ZedClient implements ProxyHandler { }; if (preconnect & PreconnectServices.PERMISSIONS_SERVICE) { - this.acl = new PermissionsServiceClient(this.endpoint, this.creds, options); + this.acl = new PermissionsServiceClient(this.endpoint, this.creds, this.options); } if (preconnect & PreconnectServices.SCHEMA_SERVICE) { - this.ns = new SchemaServiceClient(this.endpoint, this.creds, options); + this.ns = new SchemaServiceClient(this.endpoint, this.creds, this.options); } if (preconnect & PreconnectServices.WATCH_SERVICE) { - this.watch = new WatchServiceClient(this.endpoint, this.creds, options); + this.watch = new WatchServiceClient(this.endpoint, this.creds, this.options); } if (preconnect & PreconnectServices.WATCH_PERMISSIONS_SERVICE) { - this.watchPermissions = new WatchPermissionsServiceClient(this.endpoint, this.creds, options); + this.watchPermissions = new WatchPermissionsServiceClient(this.endpoint, this.creds, this.options); } if (preconnect & PreconnectServices.WATCH_PERMISSIONSETS_SERVICE) { this.watchPermissionSets = new WatchPermissionSetsServiceClient( this.endpoint, this.creds, - options, + this.options, ); } if (preconnect & PreconnectServices.EXPERIMENTAL_SERVICE) { - this.experimental = new ExperimentalServiceClient(this.endpoint, this.creds, options); + this.experimental = new ExperimentalServiceClient(this.endpoint, this.creds, this.options); } } From 7246fdcb307cc012eb1e449a0b1e30cbe0906213 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Thu, 2 Jul 2026 19:56:50 -0400 Subject: [PATCH 3/4] style: format v1.ts with oxfmt (#200) --- src/v1.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/v1.ts b/src/v1.ts index 05ae786..7454a7f 100644 --- a/src/v1.ts +++ b/src/v1.ts @@ -98,7 +98,11 @@ class ZedClient implements ProxyHandler { this.watch = new WatchServiceClient(this.endpoint, this.creds, this.options); } if (preconnect & PreconnectServices.WATCH_PERMISSIONS_SERVICE) { - this.watchPermissions = new WatchPermissionsServiceClient(this.endpoint, this.creds, this.options); + this.watchPermissions = new WatchPermissionsServiceClient( + this.endpoint, + this.creds, + this.options, + ); } if (preconnect & PreconnectServices.WATCH_PERMISSIONSETS_SERVICE) { this.watchPermissionSets = new WatchPermissionSetsServiceClient( From fbdfe0f14df8514f09a3b509f983a98315385c76 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Thu, 9 Jul 2026 01:42:12 -0400 Subject: [PATCH 4/4] test: assert retries are disabled by default and overridable (#200) Uses grpc-js channelFactoryOverride to capture the final merged channel options. Verified discriminating: fails against the pre-fix v1.ts (enable_retries undefined), passes with the fix (0), and the override case yields 1. --- src/v1.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/v1.test.ts b/src/v1.test.ts index dd6d2d2..72fcc01 100644 --- a/src/v1.test.ts +++ b/src/v1.test.ts @@ -859,3 +859,38 @@ describe("createStructFromObject unit tests", () => { ); }); }); + +describe("gRPC retry default (#200)", () => { + // Capture the channel options the client actually applies, via grpc-js's + // channelFactoryOverride hook (called with the final merged options). + function capturedChannelOptions(extra?: grpc.ClientOptions): grpc.ChannelOptions { + let captured: grpc.ChannelOptions = {}; + const client = NewClient( + "tok", + "localhost:50051", + ClientSecurity.INSECURE_LOCALHOST_ALLOWED, + PreconnectServices.PERMISSIONS_SERVICE, + { + ...extra, + channelFactoryOverride: ( + address: string, + creds: grpc.ChannelCredentials, + options: grpc.ChannelOptions, + ) => { + captured = options; + return new grpc.Channel(address, creds, options); + }, + } as grpc.ClientOptions, + ); + client.close(); + return captured; + } + + it("disables retries by default", () => { + expect(capturedChannelOptions()["grpc.enable_retries"]).toBe(0); + }); + + it("lets the caller re-enable retries", () => { + expect(capturedChannelOptions({ "grpc.enable_retries": 1 })["grpc.enable_retries"]).toBe(1); + }); +});