#grpc#golang#connect rpc

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."

By huud
0 views
~3 min read
Direct Browser gRPC Using Connect RPC

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

go
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:

javascript
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:

go
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:

go
// 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:

javascript
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 fetch or @connectrpc/connect-web

  • Serialization: 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-web package

  • Serialization: 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 fetch

  • Serialization: JSON

  • Ops burden: Medium. Extra protoc plugin, OpenAPI annotations, double routing

Recommendation

  • New Go + Web project: Connect-RPC. Zero proxy overhead, stdlib compatible, native fetch works out of box.

  • Existing non-Go gRPC backend: gRPC-Web + Envoy.

  • Public third-party REST API requirement: grpc-gateway.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.