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.
// 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)HTTP/2
Section titled “HTTP/2”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.
Limits
Section titled “Limits”- Message size. Connect doesn’t limit message size by default. Set
connect.WithReadMaxByteson 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,WriteTimeoutandIdleTimeoutcap 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’sWatchstream ends when it runs out. - Unencrypted HTTP/2. Without TLS, keep the port reachable only from the proxy in front of it.
Trying it locally
Section titled “Trying it locally”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:
cd examples/grpcgo run .In another terminal, call the health check with the Connect protocol over HTTP/1.1:
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:
curl http://localhost:8080/statusokgrpc-go servers
Section titled “grpc-go servers”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
Section titled “fasthttp”fasthttp serves HTTP/1.x only, so gRPC needs the net/http router.