gRPC-Web lets you call gRPC services directly from a web browser, but it’s not a direct translation of gRPC; it’s a fundamentally different protocol designed for browser constraints.
Let’s see it in action. Imagine a simple Greeter service defined in protobuf:
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
A typical gRPC-Web setup involves a frontend JavaScript application, a gRPC-Web proxy, and your backend gRPC server.
Here’s how a browser client might initiate a call:
// Assuming you have generated gRPC-Web client code
const request = new proto.greet.HelloRequest({ name: "Browser" });
const client = new proto.greet.GreeterPromiseClient("http://localhost:8080"); // gRPC-Web proxy URL
client.sayHello(request).then(response => {
console.log("Greeting:", response.message);
});
The browser doesn’t speak raw HTTP/2, which is gRPC’s native transport. Instead, gRPC-Web translates gRPC requests into HTTP/1.1 or HTTP/2 requests that browsers can handle. The key component enabling this is the gRPC-Web proxy.
The most common proxy is Envoy. Here’s a simplified Envoy configuration snippet for routing gRPC-Web traffic:
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: grpc_backend
# This is crucial for gRPC-Web
upgrade_configs:
- enabled: true
upgrade_type: "websocket" # Or h2c for direct HTTP/2 if supported
http_filters:
- name: envoy.filters.http.grpc_web
typed_config: {}
- name: envoy.filters.http.router
typed_config: {}
clusters:
- name: grpc_backend
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
# Your actual gRPC server address
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 9090 }
# gRPC specific configurations
typed_extension_protocol:
name: envoy.extensions.upstreams.http.v3.HttpProtocolOptions
typed_config:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
The browser client sends a POST request to the gRPC-Web proxy (e.g., http://localhost:8080/greet.Greeter/SayHello). The Content-Type header will typically be application/grpc-web-text or application/grpc-web. The grpc_web filter in Envoy then translates this into a standard gRPC request (likely HTTP/2) to your backend service.
The upgrade_configs with upgrade_type: "websocket" is a common pattern. gRPC-Web can use WebSockets to stream messages, which is more efficient than repeatedly opening HTTP connections for bidirectional streaming. If your backend and proxy support HTTP/2 directly (h2c), you can configure Envoy to upgrade to that.
The gRPC-Web protocol itself is a bit of a hack. It serializes gRPC messages into a specific format that can be sent over HTTP/1.1 POST requests or WebSockets. The grpc_web filter in Envoy (or any compatible proxy) is responsible for both receiving these gRPC-Web formatted requests and converting them into native gRPC messages for the backend, and vice-versa for responses. The fact that it can use WebSockets for streaming is key; without it, streaming would be prohibitively expensive over typical browser HTTP semantics.
The next hurdle you’ll encounter is managing inter-service communication within your backend if you start building more complex microservices.