Skip to content

gRPC and Connect

A gRPC call is a POST to /package.Service/Method over HTTP/2. Connect handlers are plain http.Handlers, so the router serves a gRPC service next to its other routes, on the same port.

Route each service with a param for the method:

r.Handle(http.MethodPost, "/grpc.health.v1.Health/{method}", handler)

grpchealth.NewHandler, like the New…Handler functions Connect generates, returns the service’s path, here /grpc.health.v1.Health/, together with the handler, so path+"{method}" is the pattern.

examples/grpc/main.go
// Command grpc serves a gRPC service and a plain HTTP endpoint from one router, on one port.
// The service is gRPC health checking from connectrpc.com/grpchealth, so the example needs no generated code.
//
// gRPC runs over HTTP/2. The server accepts HTTP/2 over TLS, and HTTP/1 and unencrypted HTTP/2 without it,
// as it would behind a proxy that terminates TLS. It serves without TLS here; ListenAndServeTLS serves with it.
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"connectrpc.com/connect"
"connectrpc.com/grpchealth"
"github.com/vardius/gorouter/v5"
)
// maxMessageBytes caps each message after decompression. Connect has no limit by default,
// so without one a small compressed request can make the handler allocate gigabytes.
const maxMessageBytes = 4 << 20
// newRouter serves the health service, ending each of its calls after callTimeout, and /status.
func newRouter(callTimeout time.Duration) *gorouter.Router {
r := gorouter.New()
r.GET("/status", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "ok")
})
// A gRPC call is a POST to /package.Service/Method, and the service's handler picks the method from that
// full path. Route the methods with a param, not Mount: Mount strips the service name from the path,
// so the handler would find no method to call.
path, health := grpchealth.NewHandler(grpchealth.NewStaticChecker(), connect.WithReadMaxBytes(maxMessageBytes))
r.Handle(http.MethodPost, path+"{method}", health)
// The server's timeouts don't end a stream that sends nothing over HTTP/1.1, as the Connect protocol
// and gRPC-Web allow, so every call gets a deadline of its own.
r.Use(path, withDeadline(callTimeout))
return r
}
// withDeadline cancels each request's context after d. Connect ends the call when its context is done.
func withDeadline(d time.Duration) gorouter.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func newServer(addr string, h http.Handler) *http.Server {
var protocols http.Protocols
protocols.SetHTTP1(true)
protocols.SetHTTP2(true)
protocols.SetUnencryptedHTTP2(true)
return &http.Server{
Addr: addr,
Handler: h,
Protocols: &protocols,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: time.Minute,
// Longer than a call's deadline, so the deadline ends a call cleanly before the server resets its stream.
WriteTimeout: 2 * time.Minute,
IdleTimeout: 2 * time.Minute,
}
}
func main() {
log.Fatal(newServer(":8080", newRouter(time.Minute)).ListenAndServe())
}

Connect clients built with connect.WithHTTPGet() send methods marked NO_SIDE_EFFECTS as GET requests. If a service has such methods, register its pattern for GET as well:

r.Handle(http.MethodGet, path+"{method}", handler)

gRPC needs HTTP/2. Setting http.Server.Protocols replaces the server’s defaults, so list every protocol it serves. The example allows HTTP/1, HTTP/2 over TLS and unencrypted HTTP/2, so the same server works with ListenAndServe behind a proxy that terminates TLS, and with ListenAndServeTLS. See HTTPS and HTTP/2 for serving over TLS.

Connect handlers also accept the Connect protocol and gRPC-Web, which run over HTTP/1.1 as well.

  • Message size. Connect doesn’t limit message size by default. Set connect.WithReadMaxBytes on every handler, as the example does. The limit applies after decompression, so a small gzipped request can’t make the server allocate gigabytes.
  • Timeouts. The server’s ReadTimeout, WriteTimeout and IdleTimeout cap slow requests, slow responses and idle connections, but over HTTP/1.1 a stream that sends nothing outlives them. So the example also gives every call to the service a deadline, with middleware on the service’s path. Pick it to fit the longest stream you serve: the health service’s Watch stream ends when it runs out.
  • Unencrypted HTTP/2. Without TLS, keep the port reachable only from the proxy in front of it.

The example is a module of its own, so its gRPC dependencies stay out of the router’s go.mod. Run it from its folder:

Terminal window
cd examples/grpc
go run .

In another terminal, call the health check with the Connect protocol over HTTP/1.1:

Terminal window
curl -H 'Content-Type: application/json' -d '{}' http://localhost:8080/grpc.health.v1.Health/Check
{"status":"SERVING_STATUS_SERVING"}

The plain HTTP route answers on the same port:

Terminal window
curl http://localhost:8080/status
ok

A *grpc.Server is an http.Handler too, and is routed the same way:

r.Handle(http.MethodPost, "/grpc.health.v1.Health/{method}", grpcServer)

grpc-go marks ServeHTTP as experimental, and documents that its performance and features may differ from grpc-go’s own HTTP/2 server. For a service that only speaks gRPC, grpcServer.Serve on its own port avoids both.

fasthttp serves HTTP/1.x only, so gRPC needs the net/http router.