Skip to content

gRPC-Web Client — Consuming gRPC Services from Browser Applications

DodaTech Updated 2026-06-28 5 min read

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

  1. Forgetting to configure CORS — browsers require CORS headers. Envoy must include Access-Control-Allow-Origin and related headers for gRPC-Web requests.
  2. 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.
  3. Not handling streaming disconnects — browser streams can disconnect due to network changes or tab visibility changes. Implement reconnection logic.
  4. Sending binary protobuf directly — gRPC-Web requires base64-encoded binary or JSON. Configure the transport for the correct wire format.
  5. Ignoring CORS preflight — OPTIONS requests must be handled by the proxy. Envoy's CORS filter handles this automatically.

Practice Questions

  1. Why can't browsers use native gRPC directly?
  2. What role does Envoy play in gRPC-Web?
  3. How does gRPC-Web handle streaming?
  4. What CORS headers are required for gRPC-Web?
  5. 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 is the difference between gRPC and gRPC-Web?

gRPC-Web is a subset of the gRPC protocol designed for browser compatibility. It uses HTTP/1.1 instead of HTTP/2 and requires an Envoy proxy to translate.

Can gRPC-Web use bidirectional streaming?

No. gRPC-Web supports unary and server-streaming only. Client-streaming and bidirectional streaming require native gRPC which browsers don't support.

What browser versions support gRPC-Web?

gRPC-Web works in all modern browsers (Chrome, Firefox, Safari, Edge) that support Fetch API and ReadableStream.

How do I debug gRPC-Web requests?

Use browser DevTools Network tab. gRPC-Web requests appear as POST requests with content-type: application/grpc-web. Response data is binary.

Can I use gRPC-Web without Envoy?

Some gRPC servers support gRPC-Web natively (e.g., grpc-web Go interceptor, Python grpclib). But Envoy is the standard, production-grade approach.

What's Next

Learn more about gRPC-Web

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro