gRPC-Gateway is a plugin that generates a reverse proxy server for gRPC services that convert Restful/JSON into gRPC and vice versa.
In other words, gRPC-Gateway will create a layer over your gRPC services that will act as a Restful/JSON service to a client. gRPC-Gateway generates code from Protocol Buffer’s definitions of gRPC services.
gRPC-Gateway is a plugin for protoc and will generate Go code from the gRPC definition.
The generated code can be used as a standalone server or mounted on an existing codebase. gRPC-Gateway is highly customizable with support for generating open API documentation from protoc files.
In this tutorial guide, we will cover both standalone servers and integration with existing code in detail. Take a look at this flow chart to get an understanding of how a gRPC gateway works.
gRPC gateways build a proxy for a gRPC service that acts as a Restful/JSON application to the client. It opens the possibility of using the same codebase for supporting both Restful/JSON and gRPC. There are two major use cases for this.
The most common gRPC-Gateway pattern is to create a single gRPC gateway server (which might be running on multiple machines) that interfaces with multiple gRPC services as a proxy for clients.
The diagram below explains the working of this service.
A gRPC gateway-generated reverse proxy is horizontally scaled to run on multiple machines and a load-balancer is used in front of these instances. A single instance can host multiple gRPC services’ reverse proxies.
gRPC-Gateway is a plugin for protoc. Before using it, the protocol buffer compiler must be installed on the system. Follow this guide on offical gRPC website to install protoc on your system according to the operating system you are using.
gRPC-Gateway uses and generates Go code. To install Go, follow the guide on the official website. Once you have installed Go on your system, you are all set to install the gRPC-Gateway plugin.
Create a directory named grpc-gateway-demo
, which will hold the gRPC-Gateway project. For building protocol buffers and generating a gRPC gateway reverse proxy, Buf will be used. You can install Buf by following the guide on the official website.
All the Protocol Buffers files will be in the proto
directory, while Go files will be in root
. For setting up the Go project, use go mod init grpc-gateway-demo
and create a main.go
file. Your project should look like this:
├── main.go ├── go.mod └── proto
Buf requires three different files to generate stubs and reverse proxies.
buf.gen.yaml
These files specify all the plugins the compiler should use and related options.
With Buf, you can simply specify the name and option in a YAML file. Buf also allows building code to use remote plugins (i.e., specified plugins will be downloaded by Buf automatically during the build and maintained by Buf on the local system).
version: v1 plugins: # generate go structs for protocol buffer defination - remote: buf.build/library/plugins/go:v1.27.1-1 out: gen/go opt: - paths=source_relative # generate gRPC stubs in golang - remote: buf.build/library/plugins/go-grpc:v1.1.0-2 out: gen/go opt: - paths=source_relative # generate reverse proxy from protocol definations - remote: buf.build/grpc-ecosystem/plugins/grpc-gateway:v2.6.0-1 out: gen/go opt: - paths=source_relative # generate openapi documentation for api - remote: buf.build/grpc-ecosystem/plugins/openapiv2:v2.6.0-1 out: gen/openapiv2
buf.yaml
This file should be in the root of all the proto files. These files specify the required dependency for compiling proto files (Google APIs, for example).
version: v1 deps: # adding well known types by google - buf.build/googleapis/googleapis
buf.work.yaml
This file specifies all the folders/directories that contain Protocol Buffer definitions in your workspaces.
version: v1 directories: - proto
Once complete, your project structure should resemble this.
├── buf.gen.yaml ├── buf.work.yaml ├── go.mod ├── main.go └── proto ├── buf.yaml
You can test your configuration by running the buf build
command in your project root.
Until now, you have set up gRPC-Gateway as a plugin, but now the question arises of how to define basic API specifications like HTTP method, URL, or request body.
For defining what these specification options are using in Protocol Buffers’ definition of an rpc
method on a service
, the following example will make it more clear.
proto/hello/hello_world.proto
:
// define syntax used in proto file syntax = "proto3"; // options used by gRPC golang plugin(not related to gRPC gateway) option go_package = "github.com/anshulrgoyal/grpc-gateway-demo;grpc_gateway_demo"; // well know type by google, gRPC gateway uses HTTP annotation. import "google/api/annotations.proto"; package hello_world; // simple message message HelloRequest { string name = 1; } message HelloReply { string message = 1; } // a gRPC service service Greeter { // SayHello is a rpc call and a option is defined for it rpc SayHello (HelloRequest) returns (HelloReply) { // option type is http option (google.api.http) = { // this is url, for RESTfull/JSON api and method // this line means when a HTTP post request comes with "/v1/sayHello" call this rpc method over this service post: "/v1/sayHello" body: "*" }; } }
The option
keyword is used to add specifications for the Rest request. The option
method is chosen and the path for that request is specified.
In above example, post
is the HTTP method for request and /v1/sayHello
is the response.
You can now build your code using the buf generate
command in the root of your project directory.
After the command completes, there should be a gen
directory in the root of your project with Go code inside. These files contain stubs for gRPC and the gRPC gateway reverse proxy. openapiv2
contains the open API documentation for Swagger UI.
gen |-- go | `-- hello | |-- hello_world.pb.go | |-- hello_world.pb.gw.go | `-- hello_world_grpc.pb.go `-- openapiv2 `-- hello `-- hello_world.swagger.json
As an example, this tutorial will implement the gRPC server in Go. Any gRPC implementations will work perfectly fine for the gRPC gateway.
The advantage of using Go is that you can run both gRPC service- and gRPC-Gateway generated code in the same process. Here is Go’s implementation for the Greeter
service.
sever/main.go:
package main import ( "context" "fmt" "log" "net" // importing generated stubs gen "grpc-gateway-demo/gen/go/hello" "google.golang.org/grpc" ) // GreeterServerImpl will implement the service defined in protocol buffer definitions type GreeterServerImpl struct { gen.UnimplementedGreeterServer } // SayHello is the implementation of RPC call defined in protocol definitions. // This will take HelloRequest message and return HelloReply func (g *GreeterServerImpl) SayHello(ctx context.Context, request *gen.HelloRequest) (*gen.HelloReply, error) { return &gen.HelloReply{ Message: fmt.Sprintf("hello %s",request.Name), },nil } func main() { // create new gRPC server server := grpc.NewServer() // register the GreeterServerImpl on the gRPC server gen.RegisterGreeterServer(server, &GreeterServerImpl{}) // start listening on port :8080 for a tcp connection if l, err := net.Listen("tcp", ":8080"); err != nil { log.Fatal("error in listening on port :8080", err) } else { // the gRPC server if err:=server.Serve(l);err!=nil { log.Fatal("unable to start server",err) } } }
The above file is a basic implementation for the gRPC service. It listens on port 8080. You can test it on any gRPC client.
Each gRPC server supported by the gRPC gateway proxy needs to be registered on it.
Under the hood, the gRPC gateway server will create a gRPC client and use it to make gRPC requests to the provided endpoint. You can provide various DailOptions
to the Register function.
proxy/main.go
package main import ( "context" "log" "net" "net/http" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" gen "grpc-gateway-demo/gen/go/hello" ) func main() { // creating mux for gRPC gateway. This will multiplex or route request different gRPC service mux:=runtime.NewServeMux() // setting up a dail up for gRPC service by specifying endpoint/target url err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()}) if err != nil { log.Fatal(err) } // Creating a normal HTTP server server:=http.Server{ Handler: mux, } // creating a listener for server l,err:=net.Listen("tcp",":8081") if err!=nil { log.Fatal(err) } // start server err = server.Serve(l) if err != nil { log.Fatal(err) } }
ServerMux
is a multiplexer that will route requests to various registered services based on the path of the JSON/Restful request.
The grpc.WithInsecure()
dial option is used to allow a service to connect to gRPC without using authentication. localhost:8080
is the URL where the gPRC service is running — since the Greet
(gRPC service build seen earlier) service is running on port 8080, localhost:8080
is used.
Once the handlers are registered, mux
is ready to handle HTTP requests. Here, the Go standard HTTP server from http
package is used. You are also free to use other implementations, and later on this article will demonstrate this using Gin with the gRPC gateway proxy.
ServerMux
implements the ServeHTTP
interface — it can be used as Handler
in the HTTP server. The server is running on port 8081.
For starting the server, just run go run proxy/main.go
in the root of your project directory.
Now, if you want to make the v1/sayHello
API a GET call inside of a POST call and pass data as a path parameter, then with a gRPC gateway setup complete, you don’t need to change anything in code — just by changing protocol buffer definitions and regenerating the stubs, you are all set to use the new API.
message HelloRequest { string name = 1; } service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { get:"/v1/sayHello/{name}" }; } }
The path provided in the above snippet is /v1/sayHello/{name}
. You can use any key from the request payload (HelloRequest
in this case) as a path parameter. If you use a GET request with the path /v1/sayHello/jane
, the request will be routed to the Greeter.sayHello
gRPC call. You can use any number of path parameters in the URL.
Now you have some basic understanding of the gRPC gateway and its setup.
The example we have used is just an introduction to the gRPC gateway, but to run something in production you need to have logging, tracing, and error handling.
For any system to be production-ready, it should have some error handling and allow some kind of error logging.
This section of the article will demonstrate how to use middleware with a gRPC gateway-generated proxy.
ServerMux
implements a Handler
interface so you can use any middleware to wrap the ServerMux
and log incoming and outgoing requests.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
To create a middleware for logging, you can extract information related to an HTTP request from *Request
and the information about the response is extracted using the httpsnoop
package.
func withLogger(handler http.Handler) http.Handler { // the create a handler return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { // pass the handler to httpsnoop to get http status and latency m:=httpsnoop.CaptureMetrics(handler,writer,request) // printing exracted data log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path) }) }
The withLogger
method will wrap the Handler interface and call snoop to extract information. Under the hood, the ServerHTTP
method is called by the httpsnoop
package.
server:=http.Server{ Handler: withLogger(mux), }
This is no different from any other handler used in the Go ecosystem. Since ServerMux
is a normal handler, any middleware available will also work with a gRPC gateway-generated reverse proxy.
gRPC gateways already come with mapping for translating gRPC error codes to HTTP status used by the client. It will automatically map well known and used gRPC codes to the HTTP status, for example.
InvalidArgument
is converted to 400
(bad request). For a complete list you can check this link. If you have custom requirements, like needing a non-conventional status code, you can use the WithErrorhandler
option that takes an error handler function — all errors will be passed to this function with the request-and-response writer.
runtime.WithErrorHandler( func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {} )
The error handler function gets the following arguments
ctx
: Context; holds metadata about executionmux
: This is ServerMux
; it holds config data about the server like which header should be passed to responsemarshaler
: Converts Protocol Buffer response to JSON responsewriter
: This is the response writer for the clientrequest
: This requests objects that contain information sent by the clienterr
: Error sent by the gRPC serviceHere is a simple example of WithErrorHandler
. In this example, the HTTP status for the request is changed to 400
when an error occurs, irrespective of the error.
mux:=runtime.NewServeMux( runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) { //creating a new HTTTPStatusError with a custom status, and passing error newError:=runtime.HTTPStatusError{ HTTPStatus: 400, Err: err, } // using default handler to do the rest of heavy lifting of marshaling error and adding headers runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError) }))
The status is changed by creating a new error and passing it to DefaultHTTPErrorHandler
. It is important to note that DefaultHTTPErrorHandler
performs a lot of work under the hood to convert the error to a valid JSON response — try to use it wherever possible.
gRPC and Restful/JSON pass metadata differently.
In Restful/JSON HTTP, headers are used to send HTTP headers, whereas gRPC abstracts out sending metadata by providing a metadata interface depending on the language used.
The gRPC gateway provides a simple mapping interface to convert gRPC metadata to HTTP headers and vice versa. It also allows for two different methods to handle header-to-metadata conversion.
Firstly,WithOutgoingHeaderMatcher
handles the header going from the gRPC gateway back to the client. It converts metadata into HTTP headers (i.e., any metadata passed by the gRPC service will be sent back to the client as HTTP headers).
var allowedHeaders=map[string]struct{}{ "x-request-id": {}, } func isHeaderAllowed(s string)( string,bool) { // check if allowedHeaders contain the header if _,isAllowed:=allowedHeaders[s];isAllowed { // send uppercase header return strings.ToUpper(s),true } // if not in the allowed header, don't send the header return s, false } // usage mux:=runtime.NewServeMux( // convert header in response(going from gateway) from metadata received. runtime.WithOutgoingHeaderMatcher(isHeaderAllowed))
This method takes a string and returns true if the header is passed to the client, or false if not.
Secondly, WithMetadata
handles incoming HTTP headers (i.e., cookies, content-type, etc.). Its most common use case is to get an authentication token and pass it to metadata. HTTP headers extracted here will be sent to the gRPC service in metadata.
mux:=runtime.NewServeMux( handle incoming headers runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD { header:=request.Header.Get("Authorization") // send all the headers received from the client md:=metadata.Pairs("auth",header) return md }),
It takes a function that takes requests and returns metadata. Be careful about headers converted to metadata since the client, browsers, load balancer, and CDN are in many of them. There are also some restrictions on keys for gRPC.
Here is a complete example:
package main import ( "context" "log" "net" "net/http" "strings" "github.com/felixge/httpsnoop" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/metadata" gen "grpc-gateway-demo/gen/go/hello" ) func withLogger(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { m:=httpsnoop.CaptureMetrics(handler,writer,request) log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path) }) } var allowedHeaders=map[string]struct{}{ "x-request-id": {}, } func isHeaderAllowed(s string)( string,bool) { // check if allowedHeaders contain the header if _,isAllowed:=allowedHeaders[s];isAllowed { // send uppercase header return strings.ToUpper(s),true } // if not in the allowed header, don't send the header return s, false } func main() { // creating mux for gRPC gateway. This will multiplex or route request different gRPC service mux:=runtime.NewServeMux( // convert header in response(going from gateway) from metadata received. runtime.WithOutgoingHeaderMatcher(isHeaderAllowed), runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD { header:=request.Header.Get("Authorization") // send all the headers received from the client md:=metadata.Pairs("auth",header) return md }), runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) { //creating a new HTTTPStatusError with a custom status, and passing error newError:=runtime.HTTPStatusError{ HTTPStatus: 400, Err: err, } // using default handler to do the rest of heavy lifting of marshaling error and adding headers runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError) })) // setting up a dail up for gRPC service by specifying endpoint/target url err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()}) if err != nil { log.Fatal(err) } // Creating a normal HTTP server server:=http.Server{ Handler: withLogger(mux), } // creating a listener for server l,err:=net.Listen("tcp",":8081") if err!=nil { log.Fatal(err) } // start server err = server.Serve(l) if err != nil { log.Fatal(err) } }
Query parameters are supported by default. You can add them in the path with the same key in message definitions. So, if you had a key named last_name
in HelloResponse
, you can enter the path v1/sayHello/anshul?last_name=goyal
without changing anything in the gateway code.
gRPC-Gateway allows you to customize if you want keys in your response in original case or camelCase
. By default it is camelCase
, but you can edit Marshaler configuration to change it.
mux:=runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ Marshaler: &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ UseProtoNames: true, EmitUnpopulated: true, }, UnmarshalOptions: protojson.UnmarshalOptions{ DiscardUnknown: true, }, }, }),)
Gin is a very popular Go web framework. You can use gRPC-Gateway with Gin, since it is just a handler. It will allow you to add additional routes on your server that may not be generated by gRPC-Gateway.
package main import ( "context" "log" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/metadata" gen "grpc-gateway-demo/gen/go/hello" ) var allowedHeaders=map[string]struct{}{ "x-request-id": {}, } func isHeaderAllowed(s string)( string,bool) { // check if allowedHeaders contain the header if _,isAllowed:=allowedHeaders[s];isAllowed { // send uppercase header return strings.ToUpper(s),true } // if not in the allowed header, don't send the header return s, false } func main() { // creating mux for gRPC gateway. This will multiplex or route request different gRPC service mux:=runtime.NewServeMux( // convert header in response(going from gateway) from metadata received. runtime.WithOutgoingHeaderMatcher(isHeaderAllowed), runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD { header:=request.Header.Get("Authorization") // send all the headers received from the client md:=metadata.Pairs("auth",header) return md }), runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) { //creating a new HTTTPStatusError with a custom status, and passing error newError:=runtime.HTTPStatusError{ HTTPStatus: 400, Err: err, } // using default handler to do the rest of heavy lifting of marshaling error and adding headers runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError) })) // setting up a dail up for gRPC service by specifying endpoint/target url err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()}) if err != nil { log.Fatal(err) } // Creating a normal HTTP server server:=gin.New() server.Use(gin.Logger()) server.Group("v1/*{grpc_gateway}").Any("",gin.WrapH(mux)) // additonal route server.GET("/test", func(c *gin.Context) { c.String(http.StatusOK,"Ok") }) // start server err = server.Run(":8081") if err != nil { log.Fatal(err) } }
Simply use the gin. WrapH
method with a wildcard path and you are ready to use gin with your server. It allows you to add a route to your server if you want. You can also add routes directly to ServerMux using HandlePath
.
err = mux.HandlePath("GET", "test", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { w.Write([]byte("ok") })
It is possible to run both services on a single port. You can do this by using the cmux
package.
cmux
will split the gRPC traffic and RestFull/JSON by differentiating between the protocol used, because gRPC will use HTTP2 and RestFull/JSON will use HTTP1.
package main import ( "context" "fmt" "log" "net" "net/http" "github.com/felixge/httpsnoop" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/soheilhy/cmux" // importing generated stubs gen "grpc-gateway-demo/gen/go/hello" "google.golang.org/grpc" ) // GreeterServerImpl will implement the service defined in protocol buffer definitions type GreeterServerImpl struct { gen.UnimplementedGreeterServer } // SayHello is the implementation of RPC call defined in protocol definitions. // This will take HelloRequest message and return HelloReply func (g *GreeterServerImpl) SayHello(ctx context.Context, request *gen.HelloRequest) (*gen.HelloReply, error) { if err:=request.Validate();err!=nil { return nil,err } return &gen.HelloReply{ Message: fmt.Sprintf("hello %s %s",request.Name,request.LastName), },nil } func main() { // create new gRPC server grpcSever := grpc.NewServer() // register the GreeterServerImpl on the gRPC server gen.RegisterGreeterServer(grpcSever, &GreeterServerImpl{}) // creating mux for gRPC gateway. This will multiplex or route request different gRPC service mux:=runtime.NewServeMux() // setting up a dail up for gRPC service by specifying endpoint/target url err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8081", []grpc.DialOption{grpc.WithInsecure()}) if err != nil { log.Fatal(err) } // Creating a normal HTTP server server:=http.Server{ Handler: withLogger(mux), } // creating a listener for server l,err:=net.Listen("tcp",":8081") if err!=nil { log.Fatal(err) } m := cmux.New(l) // a different listener for HTTP1 httpL := m.Match(cmux.HTTP1Fast()) // a different listener for HTTP2 since gRPC uses HTTP2 grpcL := m.Match(cmux.HTTP2()) // start server // passing dummy listener go server.Serve(httpL) // passing dummy listener go grpcSever.Serve(grpcL) // actual listener m.Serve() } func withLogger(handler http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { m:=httpsnoop.CaptureMetrics(handler,writer,request) log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path) }) }
This tutorial explained all the essentials required for building an excellent gRPC-Gateway reverse proxy for your gRPC service.
Since gRPC-Gateway, ServerMux is now just a handler you can build on top of by adding more middleware like body compression, authentication, and panic handling.
You can also play with the gRPC gateway configuration. All the code examples can be found here.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]