Skip to content

gRPC Proxy

GrpcProxy is a middleware that forwards requests with Content-Type: application/grpc* to HTTP/2 backends, passing all other requests through to the next layer. It wraps H2ReverseProxy and adds a content-type filter so that gRPC and non-gRPC traffic can share the same application.

Basic usage

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::proxy::GrpcProxy;
let app = App::new()
.wrap(GrpcProxy::new(["grpc-backend:50051"]));

Only requests whose Content-Type header starts with application/grpc are intercepted. All other requests fall through to the inner application.

Content-type filter

GrpcProxy matches the following content types (prefix match):

ValueExample
application/grpcbare gRPC
application/grpc+protoProtocol Buffers encoding
application/grpc+jsonJSON-encoded gRPC
application/grpc-webgRPC-Web

Any request whose Content-Type does not start with application/grpc is passed to next.execute() unchanged.

Scoping to a path prefix

Use .path_prefix() to further narrow which gRPC services are proxied:

use rust_web_server::proxy::GrpcProxy;
// Only proxy requests to the MyService gRPC service
GrpcProxy::new(["grpc-service:50051"])
.path_prefix("/svc.MyService")

Requests to other paths are passed through even if their content type is application/grpc.

Round-robin load balancing

Backend selection uses an AtomicUsize counter that increments on every forwarded request. Multiple backends are tried in order if the first fails:

GrpcProxy::new([
"grpc-backend-1:50051",
"grpc-backend-2:50051",
"grpc-backend-3:50051",
])

Combining gRPC and HTTP/2 traffic

Use GrpcProxy together with H2ReverseProxy to route mixed traffic on the same port:

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::proxy::{GrpcProxy, H2ReverseProxy};
let app = App::new()
// gRPC traffic to the gRPC backend
.wrap(GrpcProxy::new(["grpc-svc:50051"]))
// All other HTTP/2 traffic to a separate backend
.wrap(H2ReverseProxy::new(["api-svc:8080"]).path_prefix("/api"));

TLS backends (grpcs://)

Pass grpcs:// (or https://) backend URLs to reach gRPC services that require TLS — every managed cloud gRPC endpoint (GCP, AWS, Azure) uses TLS:

GrpcProxy::new([
"grpcs://grpc-svc.prod.svc.cluster.local:443",
"grpcs://grpc-svc-2.prod.svc.cluster.local:443",
])

When no port is given, grpcs:// defaults to 443. You can also write https:// — both are treated identically.

SchemeTransportDefault port
host:portplain TCP(explicit)
grpc://host:portplain TCP80
grpcs://host:portTLS443
https://host:portTLS443

TLS certificate verification uses the system WebPKI trust store (webpki-roots). Self-signed or private CA certificates are not supported without a custom build. TLS backends require the http2 Cargo feature (already required for GrpcProxy).

gRPC trailers

gRPC uses HTTP/2 trailers to carry grpc-status and grpc-message at the end of a response stream. The current implementation forwards DATA frames as-is, but HTTP/2 trailers are not yet propagated from upstream to the client.

H2ReverseProxy

GrpcProxy is a thin filter on top of H2ReverseProxy. Use H2ReverseProxy directly when you want to proxy all HTTP/2 requests regardless of content type:

use rust_web_server::proxy::H2ReverseProxy;
// Plain TCP backend
let app = App::new()
.wrap(H2ReverseProxy::new(["h2://backend:8080"])
.connect_timeout_ms(3_000)
.read_timeout_ms(60_000));
// TLS backend (h2s:// or https://)
let app = App::new()
.wrap(H2ReverseProxy::new(["h2s://api.example.com:443"])
.connect_timeout_ms(3_000)
.read_timeout_ms(60_000));

Supported backend URL schemes for H2ReverseProxy:

SchemeTransportDefault port
host:portplain TCP(explicit)
h2://host:portplain TCP80
h2s://host:portTLS + ALPN h2443
https://host:portTLS + ALPN h2443

H2ReverseProxy bridges the synchronous Middleware interface into the async tokio runtime required for HTTP/2 upstream connections via an internal block_on_isolated helper: it spawns a scoped OS thread running its own single-threaded tokio runtime and blocks that thread, rather than using tokio::task::block_in_place. This works correctly under any tokio runtime flavor — including current_thread, where block_in_place would panic — and even when called with no tokio runtime active at all (the HTTP/1.1 thread pool).