Direct Browser gRPC Using Connect RPC
"Connect-RPC lets browser native fetch talk to Go backends over HTTP without proxies. Replaces Envoy and runs on stdlib http.Handler with JSON or Protobuf."
Problem
Browsers lack HTTP/2 framing control and response trailer access. Native fetch and XHR cannot speak standard gRPC directly.
Solution: Connect-RPC
Connect protocol runs on standard HTTP/1.1, HTTP/2, HTTP/3. Speaks directly to browser fetch or Connect Web client. No proxy needed. Interoperable with standard gRPC.
Go Server
package main
import (
"context"
"net/http"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
// ponytail: import generated code from protoc-gen-connect-go
greetv1 "example/gen/greet/v1"
"example/gen/greet/v1/greetv1connect"
)
type GreetServer struct{}
func (s *GreetServer) Greet(
ctx context.Context,
req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
res := connect.NewResponse(&greetv1.GreetResponse{
Greeting: "Hello " + req.Msg.Name,
})
return res, nil
}
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
// ponytail: local cleartext only. Use TLS in production.
http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))
}
Browser Client
Native fetch with JSON payload. No extra npm packages:
const res = await fetch("http://localhost:8080/greet.v1.GreetService/Greet", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "User" }),
});
const data = await res.json();
console.log(data.greeting);
Skipped: @connectrpc/connect-web client, Protobuf binary encoding. Add when type safety and frontend streaming required.
Handling CORS
Browsers enforce same-origin policy. Connect endpoints require CORS headers.
Stdlib CORS middleware:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Connect-Protocol-Version")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Wrap handler:
// ponytail: allow-all CORS for dev. Restrict origins and headers before deploy.
http.ListenAndServe(":8080", h2c.NewHandler(corsMiddleware(mux), &http2.Server{}))
Skipped: rs/cors package. Add for dynamic origins or preflight caching.
Server Streaming with Native Fetch
Connect protocol uses chunked transfer for streams. Consume with standard ReadableStream:
const res = await fetch("http://localhost:8080/greet.v1.GreetService/GreetStream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "User" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log("Chunk:", decoder.decode(value, { stream: true }));
}
Skipped: EndStreamResponse trailer decoding. Add @connectrpc/connect-web when typed client-side streaming needed.
Alternative Approaches
gRPC-Web + Envoy: Backend uses
google.golang.org/grpc. Envoy proxy translates HTTP/1.1 gRPC-Web to HTTP/2 gRPC. High infra overhead.grpc-gateway: Protoc plugin generates in-process reverse proxy. Exposes JSON REST endpoints mapping to gRPC methods. Best when third parties need plain REST.
Decision Matrix
Connect-RPC
Transport: HTTP/1.1, HTTP/2, HTTP/3
Proxy required: No
Browser client: Native
fetchor@connectrpc/connect-webSerialization: JSON or Protobuf binary
Ops burden: None. Standard Go
http.Handler
gRPC-Web + Envoy
Transport: HTTP/1.1 or HTTP/2 translated to gRPC
Proxy required: Yes (Envoy instance)
Browser client:
grpc-webpackageSerialization: Protobuf binary or text
Ops burden: High. Envoy config, container lifecycle, network hops
grpc-gateway
Transport: HTTP/1.1 REST
Proxy required: No (runs in-process)
Browser client: Native
fetchSerialization: JSON
Ops burden: Medium. Extra protoc plugin, OpenAPI annotations, double routing
Recommendation
New Go + Web project: Connect-RPC. Zero proxy overhead, stdlib compatible, native
fetchworks out of box.Existing non-Go gRPC backend: gRPC-Web + Envoy.
Public third-party REST API requirement: grpc-gateway.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.