gRPC-Web Client — Consuming gRPC Services from Browser Applications
In this tutorial, you will learn about grpc. We cover key concepts, practical examples, and best practices to help you master this topic.
gRPC-Web enables browser applications to call gRPC services using a special protocol that bridges HTTP/1.1 (browsers) with HTTP/2 (gRPC servers) through an Envoy proxy or gRPC-Web gateway.
What You'll Learn
- gRPC-Web protocol and how it differs from native gRPC
- Setting up Envoy proxy for gRPC-Web
- Building browser clients with protobuf-generated code
- Streaming with gRPC-Web
- Error handling in browser gRPC-Web clients
- Authentication and CORS for gRPC-Web
Why It Matters
Browsers can't speak HTTP/2 natively for gRPC. gRPC-Web solves this, allowing web applications to use the same protobuf definitions and service contracts as native gRPC clients. DodaTech's Durga Antivirus Pro uses gRPC-Web for its React-based admin dashboard, sharing protobuf definitions between the web UI and backend services.
Real-World Use
A security analyst opens the Durga Antivirus Pro dashboard in Chrome. The dashboard uses gRPC-Web to call the threat list service. The same protobuf definition used by the Go backend generates the TypeScript client. The analyst sees real-time threat updates via gRPC-Web streaming.
flowchart LR
A["Browser\n(React App)"] --> B["gRPC-Web Client\n(TypeScript)"]
B --> C["Envoy Proxy\n(gRPC-Web → gRPC)"]
C --> D["gRPC Server\n(Go/Python)"]
C --> E["HTTP/1.1 + gRPC-Web"]
E --> F["HTTP/2 + Protobuf"]
style C fill:#fef3c7,stroke:#d97706
style F fill:#bbf7d0,stroke:#16a34a
Code Examples
Example 1: Envoy Proxy for gRPC-Web
static_resources:
listeners:
- name: grpc_web_listener
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: grpc_web
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, POST, OPTIONS
allow_headers: content-type,x-grpc-web,user-agent
max_age: "86400"
routes:
- match: { prefix: "/" }
route:
cluster: grpc_backend
timeout: 30s
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_backend
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: grpc-server, port_value: 50051 }
Example 2: TypeScript gRPC-Web Client
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { ThreatServiceClient } from "./proto/threat.client";
import { ThreatRequest, ThreatResponse } from "./proto/threat";
// Create transport
const transport = new GrpcWebFetchTransport({
baseUrl: "https://api.dodatech.com",
// Or use http://localhost:8080 for development
});
// Create client
const client = new ThreatServiceClient(transport);
// Unary call
async function reportThreat(request: ThreatRequest): Promise<ThreatResponse> {
try {
const { response } = await client.reportThreat(request);
return response;
} catch (error) {
if (error.code === "UNAVAILABLE") {
console.error("Service unavailable, retrying...");
}
throw error;
}
}
// Server-streaming call
function streamThreats(deviceId: string) {
const stream = client.streamThreats({ deviceId });
stream.responses.onMessage((threat) => {
console.log(`Threat: ${threat.threatName}`);
updateDashboard(threat);
});
stream.responses.onError((error) => {
console.error("Stream error:", error);
});
stream.responses.onComplete(() => {
console.log("Stream completed");
});
return stream; // Call stream.cancel() to stop
}
Example 3: React Component with gRPC-Web
import React, { useState, useEffect, useRef } from "react";
import { ThreatServiceClient } from "./proto/threat.client";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
const transport = new GrpcWebFetchTransport({
baseUrl: process.env.REACT_APP_API_URL || "http://localhost:8080",
});
const client = new ThreatServiceClient(transport);
interface Threat {
threatId: string;
threatName: string;
severity: string;
detectedAt: string;
}
function ThreatDashboard() {
const [threats, setThreats] = useState<Threat[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const streamRef = useRef<any>(null);
// Unary call for initial data
useEffect(() => {
async function loadThreats() {
try {
const { response } = await client.listThreats({
pageSize: 50,
});
setThreats(response.threats);
setLoading(false);
} catch (err) {
setError(`Failed to load threats: ${err.message}`);
setLoading(false);
}
}
loadThreats();
}, []);
// Streaming for real-time updates
useEffect(() => {
const stream = client.streamThreats({});
stream.responses.onMessage((threat) => {
setThreats((prev) => [threat, ...prev].slice(0, 100));
});
stream.responses.onError((err) => {
console.error("Stream error:", err);
});
streamRef.current = stream;
return () => {
stream.cancel();
};
}, []);
if (loading) return <div className="loading">Loading threats...</div>;
if (error) return <div className="error">{error}</div>;
return (
<div className="dashboard">
<h2>Live Threat Feed</h2>
<div className="threat-list">
{threats.map((threat) => (
<div key={threat.threatId}
className={`threat severity-${threat.severity.toLowerCase()}`}>
<span className="name">{threat.threatName}</span>
<span className="severity">{threat.severity}</span>
<span className="time">
{new Date(threat.detectedAt).toLocaleTimeString()}
</span>
</div>
))}
</div>
</div>
);
}
Common Mistakes
- Forgetting to configure CORS — browsers require CORS headers. Envoy must include Access-Control-Allow-Origin and related headers for gRPC-Web requests.
- Using native gRPC in browsers — browsers don't support the gRPC HTTP/2 protocol. Always use gRPC-Web with an Envoy proxy or gRPC-Web-compatible gateway.
- Not handling streaming disconnects — browser streams can disconnect due to network changes or tab visibility changes. Implement reconnection logic.
- Sending binary protobuf directly — gRPC-Web requires base64-encoded binary or JSON. Configure the transport for the correct wire format.
- Ignoring CORS preflight — OPTIONS requests must be handled by the proxy. Envoy's CORS filter handles this automatically.
Practice Questions
- Why can't browsers use native gRPC directly?
- What role does Envoy play in gRPC-Web?
- How does gRPC-Web handle streaming?
- What CORS headers are required for gRPC-Web?
- How do you handle authentication in gRPC-Web clients?
Challenge: Build a React dashboard that uses gRPC-Web for both initial data loading and real-time streaming. Include authentication via JWT in metadata, reconnection logic for streams, and error handling with user-friendly messages.
Mini Project
Build a complete gRPC-Web frontend for a threat management system with: Envoy proxy for gRPC-Web translation, React + TypeScript gRPC-Web client, unary calls for CRUD operations, server-streaming for real-time alerts, CORS configuration, and JWT authentication via metadata.
FAQ
What's Next
Learn more about gRPC-Web
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro