gRPC Gateway
Expose unary gRPC methods over JSON HTTP routes.
gRPC Gateway
The grpcgateway package maps gRPC-style method paths to JSON HTTP handlers.
It handles JSON to protobuf conversion and delegates the call to an invoker.
import "github.com/Dziqha/ply/grpcgateway"
gateway, err := grpcgateway.New(grpcgateway.Options{
Resolver: func(method string) (proto.Message, proto.Message, error) {
switch method {
case "/users.v1.UserService/GetUser":
return &usersv1.GetUserRequest{}, &usersv1.GetUserResponse{}, nil
default:
return nil, nil, grpcgateway.ErrUnknownMethod
}
},
Invoker: func(ctx context.Context, method string, request, response proto.Message) error {
return conn.Invoke(ctx, method, request, response)
},
})
if err != nil {
log.Fatal(err)
}
gateway.Mount(app, "/grpc")conn is a grpc.ClientConnInterface, such as *grpc.ClientConn. The
gateway package does not import the gRPC runtime; the invoker is supplied by
the application.
HTTP mapping
A request to:
POST /grpc/users.v1.UserService/GetUser
Content-Type: application/json
{"id": "42"}is converted to the full method /users.v1.UserService/GetUser. The request
JSON is unmarshaled into the request message, passed to the invoker, and the
response message is marshaled back to JSON.
HTTP/1.1 200 OK
Content-Type: application/json
{"user":{"id":"42","name":"Ada"}}Mount registers POST <prefix>/* and hides the catch-all route from the
OpenAPI document. The returned route can be unhidden if the gateway needs to
appear in generated documentation.
Handler without Mount
Handler can be mounted on a service-specific route:
app.Post("/users.v1.UserService/*", gateway.Handler())The path must contain exactly <service>/<method> after the optional prefix.
Requests with any other shape return 404.
Options
| Option | Default | Purpose |
|---|---|---|
Resolver | required | Returns fresh request and response messages |
Invoker | required | Calls the gRPC method |
MaxBodyBytes | 4 MiB | Maximum JSON request body |
Marshal | protojson.MarshalOptions{} | Response JSON options |
Unmarshal | protojson.UnmarshalOptions{} | Request JSON options |
StatusMapper | none | Maps invoker errors to HTTP status and message |
The resolver must return a new request and response message for each call. Protobuf messages must not be shared across concurrent requests.
Request context
RequestContext returns the Ply context from the invoker context. Use it to
read request headers or values set by middleware:
Invoker: func(ctx context.Context, method string, request, response proto.Message) error {
if c := grpcgateway.RequestContext(ctx); c != nil {
token := c.Header("Authorization")
// Attach the token to the outgoing gRPC metadata.
}
return conn.Invoke(ctx, method, request, response)
},ctx also carries cancellation and deadlines from the HTTP request.
Errors
By default, invoker errors return 500 without exposing the error text. Return
*grpcgateway.Error for an explicit HTTP status:
return &grpcgateway.Error{
Status: http.StatusNotFound,
Message: "user not found",
Err: err,
}Use StatusMapper to translate gRPC status codes:
StatusMapper: func(err error) (int, string) {
st, ok := status.FromError(err)
if !ok {
return 500, "internal server error"
}
switch st.Code() {
case codes.NotFound:
return 404, st.Message()
case codes.InvalidArgument:
return 400, st.Message()
case codes.PermissionDenied:
return 403, st.Message()
default:
return 500, "internal server error"
}
},status and codes are from google.golang.org/grpc, which the application
already uses when it has a gRPC client.
Notes
- Only unary methods are supported. Client streaming, server streaming, and bidirectional streaming need a different transport.
- Requests use
application/jsonor a+jsonmedia type. Other content types return415. - An empty request body is treated as
{}. protojsonrejects unknown fields by default. SetUnmarshal.DiscardUnknownonly when the API must accept forward-compatible payloads.- Authentication middleware should run before the gateway route and attach the identity to the request context.

Ply