gRPC-Web: Bringing gRPC to the Browser

Standard gRPC relies on HTTP/2 features that browsers do not fully expose — specifically, the ability to send HTTP/2 trailers and to access raw binary frames. gRPC-Web solves this by defining a protocol that works over HTTP/1.1 and HTTP/2 without requiring direct trailer support. ASP.NET Core has built-in middleware to serve gRPC-Web alongside regular gRPC, meaning you can use a single service implementation for both internal and browser-facing traffic.

The Problem

Browsers communicate through the Fetch and XMLHttpRequest APIs. Neither gives you the low-level HTTP/2 frame access that gRPC's wire protocol requires. Specifically:

The gRPC-Web specification wraps gRPC messages in a format that works within these constraints. Trailers are encoded into the response body, and the binary payload is optionally base64-encoded.

Enabling gRPC-Web in ASP.NET Core

The Grpc.AspNetCore.Web package provides middleware that translates between gRPC-Web and native gRPC. No changes to your service implementation are needed.

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("https://myapp.example.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding");
    });
});

var app = builder.Build();

app.UseRouting();
app.UseCors();
app.UseGrpcWeb(); // Must be between UseRouting and MapGrpcService

app.MapGrpcService<WeatherForecasterService>()
   .EnableGrpcWeb()
   .RequireCors();

app.Run();

The key points:

  1. UseGrpcWeb() adds the middleware that handles protocol translation.
  2. EnableGrpcWeb() on the service endpoint opts that service in.
  3. CORS is essential — browser requests to a different origin will be blocked without it.

You can also enable gRPC-Web globally for all services instead of per-endpoint:

Example.cs
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

Calling from JavaScript or TypeScript

On the browser side, the grpc-web npm package provides a client. You generate TypeScript stubs from your .proto file using protoc with the grpc-web plugin:

terminal
protoc -I=. weather.proto \
  --js_out=import_style=commonjs:./generated \
  --grpc-web_out=import_style=typescript,mode=grpcwebtext:./generated

Then call the service from your application:

script.ts
import { WeatherForecasterClient } from './generated/WeatherServiceClientPb';
import { ForecastRequest } from './generated/weather_pb';

const client = new WeatherForecasterClient('https://api.example.com');

const request = new ForecastRequest();
request.setCity('Edinburgh');
request.setDays(3);

// Unary call
client.getForecast(request, {}, (err, response) => {
  if (err) {
    console.error(err.message);
    return;
  }
  console.log(`${response.getCity()}: ${response.getTemperatureC()}C`);
});

// Server streaming
const stream = client.streamForecasts(request, {});
stream.on('data', (response) => {
  console.log(`${response.getDate()}: ${response.getTemperatureC()}C`);
});
stream.on('error', (err) => {
  console.error(err.message);
});

Note that gRPC-Web only supports unary and server streaming calls. Client streaming and bidirectional streaming are not available — the browser APIs simply cannot support them.

Calling from a .NET Client

You can also use gRPC-Web from a .NET client. This is useful when your client cannot use HTTP/2 — for example, in Blazor WebAssembly or behind certain proxies and load balancers that only support HTTP/1.1.

Example.cs
var handler = new GrpcWebHandler(new HttpClientHandler());

using var channel = GrpcChannel.ForAddress("https://api.example.com", new GrpcChannelOptions
{
    HttpHandler = handler
});

var client = new WeatherForecaster.WeatherForecasterClient(channel);
var reply = await client.GetForecastAsync(
    new ForecastRequest { City = "Glasgow", Days = 5 });

The GrpcWebHandler wraps a standard HttpClientHandler and translates calls to the gRPC-Web wire format automatically. This is the approach Blazor WebAssembly applications use, since the browser's networking stack does not expose HTTP/2 trailers.

Deployment Considerations

Most reverse proxies (NGINX, Envoy, Azure App Service) handle gRPC-Web without special configuration because it uses standard HTTP content types. However, keep these points in mind:

When to Use gRPC-Web

gRPC-Web makes sense when you already have gRPC services and want to expose them to browser clients without building a separate REST layer. It eliminates the translation overhead of maintaining two API surfaces. For greenfield browser-facing APIs with no existing gRPC infrastructure, REST or GraphQL may be simpler starting points — but if gRPC is already your backbone, gRPC-Web is the pragmatic choice.