protoc-gen-gohttp is a plugin of protoc that for using a service
of Protocol Buffers as http.Handler definition.
The generated interface is compatible with the interface generated by the gRPC plugin.
In addition to this plugin, you need the protoc command and the proto-gen-go plugin.
The code generated by this plugin imports only the standard library, google.golang.org/protobuf
and google.golang.org/grpc
.
The converted http.Handler checks Content-Type Header, and changes Marshal/Unmarshal packages. The correspondence table is as follows.
Content-Type | package |
---|---|
application/json | google.golang.org/protobuf/encoding/protojson |
application/protobuf | google.golang.org/protobuf/proto |
application/x-protobuf | google.golang.org/protobuf/proto |
go get -u github.com/nametake/protoc-gen-gohttp
And install dependent tools. (e.g. macOS)
brew install protobuf
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
protoc --go_out=. --gohttp_out=. *.proto
You can execute examples with the following command.
make gen_examples
make run_examples
You can confirm the operation with the following command.
curl -H "Content-Type: application/json" localhost:8080/sayhello -d '{"name": "john"}'
curl -H "Content-Type: application/json" localhost:8080/greeter/sayhello -d '{"name": "john"}'
Define greeter.proto.
syntax = "proto3";
package helloworld;
option go_package = "main";
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
From greeter.proto you defined, use the following command to generate greeter.pb.go and greeter.http.go.
protoc --go_out=plugins=grpc:. --gohttp_out=. examples/greeter.proto
Using the generated Go file, implement as follows.
// EchoGreeterServer has implemented the GreeterServer interface that created from the service in proto file.
type EchoGreeterServer struct {
}
// SayHello implements the GreeterServer interface method.
// SayHello returns a greeting to the name sent.
func (s *EchoGreeterServer) SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
return &HelloReply{
Message: fmt.Sprintf("Hello, %s!", req.Name),
}, nil
}
func main() {
// Create the GreeterServer.
srv := &EchoGreeterServer{}
// Create the GreeterHTTPConverter generated by protoc-gen-gohttp.
// This converter converts the GreeterServer interface that created from the service in proto to http.HandlerFunc.
conv := NewGreeterHTTPConverter(srv)
// Register SayHello HandlerFunc to the server.
// If you do not need a http handle callback, pass nil as argument.
http.Handle("/sayhello", conv.SayHello(logCallback))
// If you want to create a path from Proto's service name and method name, use the SayHelloWithName method.
// In this case, the strings 'Greeter' and 'SayHello' are returned.
http.Handle(restPath(conv.SayHelloWithName(logCallback)))
log.Fatal(http.ListenAndServe(":8080", nil))
}
// logCallback is called when exiting ServeHTTP
// and receives Context, ResponseWriter, Request, service argument, service return value and error.
func logCallback(ctx context.Context, w http.ResponseWriter, r *http.Request, arg, ret proto.Message, err error) {
log.Printf("INFO: call %s: arg: {%v}, ret: {%s}", r.RequestURI, arg, ret)
// YOU MUST HANDLE ERROR
if err != nil {
log.Printf("ERROR: %v", err)
w.WriteHeader(http.StatusInternalServerError)
p := status.New(codes.Unknown, err.Error()).Proto()
switch r.Header.Get("Content-Type") {
case "application/protobuf", "application/x-protobuf":
buf, err := proto.Marshal(p)
if err != nil {
return
}
if _, err := io.Copy(w, bytes.NewBuffer(buf)); err != nil {
return
}
case "application/json":
buf, err := protojson.Marshal(p)
if err != nil {
return
}
if _, err := io.Copy(w, bytes.NewBuffer(buf)); err != nil {
return
}
default:
}
}
}
func restPath(service, method string, hf http.HandlerFunc) (string, http.HandlerFunc) {
return fmt.Sprintf("/%s/%s", strings.ToLower(service), strings.ToLower(method)), hf
}
protoc-gen-gohttp generates the following interface.
type GreeterHTTPService interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
This interface is compatible with the interface generated by gRPC plugin.
protoc-gen-gohttp supports google.api.HttpRule option.
When the Service is defined using HttpRule, Converter implements the {RpcName}HTTPRule
method. {RpcName}HTTPRule
method returns Request Method, Path and http.HandlerFunc.
In the following example, Converter implements GetMessageHTTPRule
. GetMessageHTTPRule
returns http.MethodGet
, "/v1/messages/{message_id}"
and http.HandlerFunc.
syntax = "proto3";
package example;
option go_package = "main";
import "google/api/annotations.proto";
service Messaging {
rpc GetMessage(GetMessageRequest) returns (GetMessageResponse) {
option (google.api.http).get = "/v1/messages/{message_id}";
}
}
message GetMessageRequest {
string message_id = 1;
string message = 2;
repeated string tags = 3;
}
message GetMessageResponse {
string message_id = 1;
string message = 2;
repeated string tags = 4;
}
{RpcName}HTTPRule
method is intended for use with HTTP libraries like go-chi/chi and gorilla/mux as follows:
type Messaging struct{}
func (m *Messaging) GetMessage(ctx context.Context, req *GetMessageRequest) (*GetMessageResponse, error) {
return &GetMessageResponse{
MessageId: req.MessageId,
Message: req.Message,
Tags: req.Tags,
}, nil
}
func main() {
conv := NewMessagingHTTPConverter(&Messaging{})
r := chi.NewRouter()
r.Method(conv.GetMessageHTTPRule(nil))
log.Fatal(http.ListenAndServe(":8080", r))
}
protoc-gen-gohttp parses Get Method according to google.api.HttpRule option. Therefore, you can pass values to the server in the above example with query string like /v1/messages/abc1234?message=hello&tags=a&tags=b
.
When you actually execute the above server and execute curl -H "Content-Type: application/json" "localhost:8080/v1/messages/abc1234?message=hello&tags=a&tags=b"
, the following JOSN is returned.
{
"messageId": "abc1234",
"message": "hello",
"tags": ["a", "b"]
}
A http handle callback is a function to handle RPC calls with HTTP.
It is called when the end of the generated code is reached without error or when an error occurs.
The callback is passed HTTP context and http.ResponseWriter and http.Request, RPC arguments and return values, and error.
RPC arguments and return values, and errors may be nil. Here's when nil is passed:
Timing | RPC argument | RPC return value | error |
---|---|---|---|
When an error occurs before calling RPC | nil | nil | err |
When RPC returns an error | arg | nil | err |
When an error occurs after calling RPC | arg | ret | err |
When no error occurred | arg | ret | nil |
You MUST HANDLE ERROR in the callback. If you do not handle it, the error is ignored.
If nil is passed to the callback, the error is always handled as an InternalServerError.
The convert method can receive multiple grpc.UnaryServerInterceptor.
Execution is done in left-to-right order, including passing of context.
For example, it is executed in the order described in the comments in the following example.
conv := NewGreeterHTTPConverter(&EchoGreeterServer{})
conv.SayHello(nil,
grpc.UnaryServerInterceptor(
func(ctx context.Context, arg interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// one (before RPC is called)
ret, err := handler(ctx, arg)
// four (after RPC is called)
return ret, err
},
),
grpc.UnaryServerInterceptor(
func(ctx context.Context, arg interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// two (before RPC is called)
ret, err := handler(ctx, arg)
// three (after RPC is called)
return ret, err
},
),
)
If you passed interceptors, you must call handler to return the correct return type.
If interceptors return the return value that is not expected by the RPC, http.Handler will pass an error like the following to the http handle callback and exit.
/helloworld.Greeter/SayHello: interceptors have not return HelloReply
- Streaming API
- Not create a convert method.
- HttpRule field below
enum
type query stringmap
type query string