grpc / grpc/grpc-rust

Support for registering the same gRPC service on different paths with different trait implementations

Open
#2,296 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
Rust
Stars
12.5k
Forks
1.3k
Avg merge
4d 7h
Merged PRs (30d)
24

Description

Feature Request

### Crates

`tonic` (transport/client channel functionality)

### Motivation

I need to register the same gRPC service (generated from the same `.proto` file) on different paths using different implementations of its trait. This is useful for creating different "front-ends" that reuse the same protocol definitions while having different business logic implementations, avoiding protocol file duplication across packages/namespaces.

**Use cases include:**
- **Multi-tenant services**: Same protocol, different data access per tenant
- **Admin vs User interfaces**: Same operations, different authorization/business rules
- **API versioning**: Different implementations of the same service contract
- **Environment-specific behavior**: Same protocol with different implementations for dev/staging/prod

**Server-side works perfectly** using `ServiceBuilder` and `AxumRouter::nest_service`:

```rust
// Admin implementation at /admin path
let admin_service = ServiceBuilder::new()
.layer(auth_layer)
.layer(admin_auth_layer)
.service(CustomerServer::new(AdminGrpcServer::new(
ClientDbRepository::new(pool.clone()),
)));

let admin_route = AxumRouter::new().nest_service(
"/admin",
admin_service.map_request(|req: http::Request| req.map(Body::new)),
);

// Regular implementation at root path
let customer_service = ServiceBuilder::new()
.layer(auth_layer.clone())
.service(CustomerServer::new(CustomerGrpcServer::new(/* different impl */)));
```

**Client-side doesn't work** - the path in the channel URI is ignored:

```rust
let addr = format!("http://localhost:{port}/admin/"); // Path is ignored
let channel = Channel::builder(addr.parse()?).connect().await?;
let admin_client = CustomerClient::with_interceptor(channel, interceptor);
// Requests go to root "/" instead of "/admin/"
```

### Proposal

Modify `Channel::builder()` and the underlying client implementation to respect and preserve the path component of the URI when making gRPC requests.

**Implementation approach:**
1. Parse and store the path component from the URI during channel creation
2. Prepend the stored path to all outgoing gRPC method calls
3. Ensure path preservation works with interceptors and other middleware
4. Not sure if doable,

**Example of desired behavior:**
```rust
// Channel created with path should preserve it for all requests
let channel = Channel::builder("http://localhost:8080/admin/".parse()?).connect().await?;
let client = CustomerClient::new(channel);

// This call should go to POST /admin/cheapo.Customer/ListInstances
// instead of POST /cheapo.Customer/ListInstances
client.list_instances(request).await?;
```

**Benefits:**
- Enables protocol reuse across different service implementations
- Reduces code duplication and maintenance overhead
- Provides cleaner separation of concerns for multi-tenant architectures
- Maintains backward compatibility (channels without paths work as before)

### Alternatives

**1. Manual path handling in interceptors:**
```rust
let client = CustomerClient::with_interceptor(channel, |mut req| {
let uri = req.uri_mut();
*uri = format!("/admin{}", uri.path()).parse().unwrap();
Ok(req)
});
```
*Drawbacks:* Requires manual path manipulation in every client, error-prone, doesn't feel like idiomatic tonic usage.

**2. Separate proto files/packages for different implementations:**
```protobuf
// admin.proto
service AdminCustomer { /* same methods */ }
// customer.proto
service Customer { /* same methods */ }
```
*Drawbacks:* Code duplication, maintenance burden, violates DRY principle, requires keeping multiple proto definitions in sync.

**3. Server-side path stripping:**
Modify server to strip paths before routing to services.
*Drawbacks:* Loses the benefit of path-based service differentiation, requires server-side workarounds, doesn't solve the architectural goal.

**4. Using different ports for different implementations:**
*Drawbacks:* Complicates deployment, requires more infrastructure resources, doesn't scale well with many service variants.

The proposed solution was chosen because it's the most natural and idiomatic approach that aligns with how HTTP clients typically handle base paths, while maintaining full backward compatibility.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.