Skip to content

Commit

Permalink
Add error response and support for serialising custom messages (#8)
Browse files Browse the repository at this point in the history
* Implement error response with custom details

* Create separate protobuf message for error status.

* Remove unused attribute
  • Loading branch information
MartinKuzma authored Aug 21, 2024
1 parent f8ae560 commit 9163358
Show file tree
Hide file tree
Showing 29 changed files with 749 additions and 261 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ docker:
go mod vendor
docker build . -t grpc-rest-proxy-test

generate:
buf generate

.PHONY: all default build test fmt lint build-only test-only race cover coverprofile clean
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,23 @@ spec:
ports:
- name: web-port
containerPort: 8080
```
### Error handling
On error, the proxy returns an HTTP status code and JSON response body. JSON is defined using our [Error protobuf message](https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto). It contains code, message and details.
The backend endpoint can define its own protobuf messages containing details of the error and return it in standard grpc status. The proxy takes these messsages and serializes them into JSON response.
```json
{
"code": 404,
"message": "User name not found.",
"details": [
{
"@type": "type.googleapis.com/user.v1.GetUserError",
"username": "John1234",
"recommendation": "Please check the username and try again."
}
]
}
```
11 changes: 11 additions & 0 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: v2
managed:
enabled: true
disable:
- module: buf.build/googleapis/googleapis
plugins:
- remote: buf.build/protocolbuffers/go:v1.34.2
out: pkg
opt: paths=source_relative
inputs:
- directory: protos
11 changes: 11 additions & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: v2
modules:
- path: protos
deps:
- buf.build/googleapis/googleapis
lint:
use:
- DEFAULT
breaking:
use:
- FILE
238 changes: 156 additions & 82 deletions cmd/examples/grpcserver/gen/user/v1/user.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions cmd/examples/grpcserver/proto/user/v1/user.proto
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,9 @@ message Job {
string job_area = 2;
string job_title = 3;
string job_type = 4;
}
message GetUserError {
string username = 1;
string recommendation = 2;
}
15 changes: 14 additions & 1 deletion cmd/examples/grpcserver/userservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"sync"

pb "github.com/eset/grpc-rest-proxy/cmd/examples/grpcserver/gen/user/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type userService struct {
Expand Down Expand Up @@ -37,7 +39,18 @@ func (s *userService) GetUser(ctx context.Context, request *pb.GetUserRequest) (
}, nil
}
}
return &pb.GetUserResponse{}, nil

errDetail := &pb.GetUserError{
Username: request.Username,
Recommendation: "Please check the username and try again.",
}

errStatus, err := status.New(codes.NotFound, "User name not found.").WithDetails(errDetail)
if err != nil {
return nil, err
}

return nil, errStatus.Err()
}

func (s *userService) GetUsers(ctx context.Context, request *pb.GetUserRequest) (*pb.GetUsersResponse, error) {
Expand Down
49 changes: 22 additions & 27 deletions cmd/service/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ import (
"github.com/eset/grpc-rest-proxy/pkg/repository/descriptors"
"github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder"
"github.com/eset/grpc-rest-proxy/pkg/service/protoparser"
routerPkg "github.com/eset/grpc-rest-proxy/pkg/service/router"
"github.com/eset/grpc-rest-proxy/pkg/transport"
"github.com/eset/grpc-rest-proxy/pkg/transport/http"
routerPkg "github.com/eset/grpc-rest-proxy/pkg/transport/router"
)

type App struct {
conf *Config
serverHTTP *http.Server
router *routerPkg.ReloadableRouter
descriptorsRepo descriptors.Descriptors
gateways *gateways
reloader *transport.EndpointReloader
}

type gateways struct {
Expand All @@ -49,14 +49,13 @@ func New(ctx context.Context, conf *Config) (*App, error) {
return nil, jErrors.Trace(err)
}

router, err := app.createRouter(ctx)
endpointProxy, err := app.createProxyEndpoint(ctx)
if err != nil {
return nil, jErrors.Annotate(jErrors.Trace(err), "failed to create router")
}
app.router = routerPkg.WithWrapper(router)

app.reloader = transport.NewEndpointReloader(endpointProxy)
app.createHTTPServer()

return app, nil
}

Expand All @@ -72,33 +71,21 @@ func createGateways(conf *Config) (*gateways, error) {
}

func (app *App) createHTTPServer() {
routerContext := &transport.Context{
Router: app.router,
GrcpClient: app.gateways.grpcClient,
JSONEncoder: jsonencoder.New(app.conf.Service.JSONEncoder),
}
handler := transport.NewHandler(routerContext, logging.Default())
handler := transport.NewHandler(app.reloader)
app.serverHTTP = http.NewServer(app.conf.Transport.HTTP.Server, handler)
}

func (app *App) createRouter(ctx context.Context) (*routerPkg.Router, error) {
router, err := app.getRouterWithRoutes(ctx)
if err != nil {
return nil, jErrors.Trace(err)
}
return router, nil
}

func (app *App) reloadRouter(ctx context.Context, r *routerPkg.ReloadableRouter) error {
routerRoutes, err := app.getRouterWithRoutes(ctx)
func (app *App) reloadEndpoint(ctx context.Context) error {
endpoint, err := app.createProxyEndpoint(ctx)
if err != nil {
return jErrors.Trace(err)
}
r.SetRouter(routerRoutes)

app.reloader.Set(endpoint)
return nil
}

func (app *App) getRouterWithRoutes(ctx context.Context) (*routerPkg.Router, error) {
func (app *App) createProxyEndpoint(ctx context.Context) (*transport.ProxyEndpoint, error) {
fileDescriptorSet, err := app.descriptorsRepo.GetProtoFileDescriptorSet(ctx)
if err != nil {
return nil, jErrors.Annotate(jErrors.Trace(err), "failed to retrieve proto descriptors from source")
Expand All @@ -109,16 +96,24 @@ func (app *App) getRouterWithRoutes(ctx context.Context) (*routerPkg.Router, err
return nil, jErrors.Trace(jErrors.New(parseResult.ErrorsString()))
}

routerRoutes := routerPkg.NewRouter()
router := routerPkg.NewRouter()

for _, route := range parseResult.Routes {
err = routerRoutes.Push(route)
err = router.Push(route)
if err != nil {
return nil, jErrors.Trace(err)
}
logging.Info(fmt.Sprintf("Added route: [%s] %s", routerPkg.MethodToString(route.Method()), route.Path()))
}
return routerRoutes, nil

encoder := jsonencoder.New(app.conf.Service.JSONEncoder, parseResult.TypeResolver)

return transport.NewProxyEndpoint(
logging.Default(),
router,
app.gateways.grpcClient,
encoder,
), nil
}

func (app *App) listenForSignal(ctx context.Context, sigUsr1 <-chan os.Signal) {
Expand All @@ -129,7 +124,7 @@ func (app *App) listenForSignal(ctx context.Context, sigUsr1 <-chan os.Signal) {
case <-sigUsr1:
logging.Info("reload signal received")

err := app.reloadRouter(ctx, app.router)
err := app.reloadEndpoint(ctx)
if err != nil {
logging.Error(jErrors.Details(jErrors.Trace(err)))
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/service/jsonencoder/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package jsonencoder
import (
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoregistry"

jErrors "github.com/juju/errors"
)
Expand All @@ -19,9 +20,15 @@ type Encoder struct {
opts protojson.MarshalOptions
}

func New(cfg *Config) Encoder {
// New creates a new JSON encoder.
// Type resolver is used to resolve types of messages and can be nil in which case the default resolver is used.
func New(cfg *Config, typeResolver *protoregistry.Types) Encoder {
return Encoder{
opts: protojson.MarshalOptions{EmitUnpopulated: cfg.EmitUnpopulated, EmitDefaultValues: cfg.EmitDefaultValues},
opts: protojson.MarshalOptions{
EmitUnpopulated: cfg.EmitUnpopulated,
EmitDefaultValues: cfg.EmitDefaultValues,
Resolver: typeResolver,
},
}
}

Expand Down
56 changes: 55 additions & 1 deletion pkg/service/protoparser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"errors"
"strings"

"github.com/eset/grpc-rest-proxy/pkg/transport/router"
"github.com/eset/grpc-rest-proxy/pkg/service/router"

jErrors "github.com/juju/errors"
"google.golang.org/genproto/googleapis/api/annotations"
Expand All @@ -16,13 +16,15 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
)

func ParseFileDescSets(fdSets []*descriptorpb.FileDescriptorSet) ParseResult {
result := ParseResult{
FileRegistry: &protoregistry.Files{},
TypeResolver: &protoregistry.Types{},
}

// Register default types.
Expand Down Expand Up @@ -67,6 +69,12 @@ func registerFile(fd protoreflect.FileDescriptor, result *ParseResult) {
}

func ParseFileDesc(fd protoreflect.FileDescriptor, result *ParseResult) {
err := registerTypes(fd, result)
if err != nil {
result.AddError(jErrors.Trace(err))
return
}

services := fd.Services()
for i := 0; i < services.Len(); i++ {
parseServiceDesc(services.Get(i), result)
Expand Down Expand Up @@ -183,3 +191,49 @@ func getPattern(rule *annotations.HttpRule) (router.MethodType, string, error) {

return router.UnknownMethod, "", jErrors.Errorf("unknown method")
}

func registerTypes(fd protoreflect.FileDescriptor, result *ParseResult) error {
msgs := fd.Messages()
for idx := 0; idx < msgs.Len(); idx++ {
msg := msgs.Get(idx)
_, err := result.TypeResolver.FindMessageByName(msg.FullName())
if err == nil {
continue
}

err = result.TypeResolver.RegisterMessage(dynamicpb.NewMessageType(msg))
if err != nil {
return jErrors.Trace(err)
}
}

enums := fd.Enums()
for idx := 0; idx < enums.Len(); idx++ {
enum := enums.Get(idx)
_, err := result.TypeResolver.FindEnumByName(enum.FullName())
if err == nil {
continue
}

err = result.TypeResolver.RegisterEnum(dynamicpb.NewEnumType(enum))
if err != nil {
return jErrors.Trace(err)
}
}

exts := fd.Extensions()
for idx := 0; idx < exts.Len(); idx++ {
ext := exts.Get(idx)
_, err := result.TypeResolver.FindExtensionByName(ext.FullName())
if err == nil {
continue
}

err = result.TypeResolver.RegisterExtension(dynamicpb.NewExtensionType(ext))
if err != nil {
return jErrors.Trace(err)
}
}

return nil
}
3 changes: 2 additions & 1 deletion pkg/service/protoparser/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ package protoparser
import (
"strings"

"github.com/eset/grpc-rest-proxy/pkg/transport/router"
"github.com/eset/grpc-rest-proxy/pkg/service/router"

"google.golang.org/protobuf/reflect/protoregistry"
)

type ParseResult struct {
FileRegistry *protoregistry.Files
TypeResolver *protoregistry.Types
Routes []*router.Route
Errors []error
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"slices"
"testing"

"github.com/eset/grpc-rest-proxy/pkg/service/router/pattern"
"github.com/eset/grpc-rest-proxy/pkg/service/transformer"
"github.com/eset/grpc-rest-proxy/pkg/transport/router/pattern"

"github.com/stretchr/testify/require"
)
Expand Down
File renamed without changes.
13 changes: 12 additions & 1 deletion pkg/transport/router/router.go → pkg/service/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package router

import (
routePattern "github.com/eset/grpc-rest-proxy/pkg/service/router/pattern"
"github.com/eset/grpc-rest-proxy/pkg/service/transformer"
routePattern "github.com/eset/grpc-rest-proxy/pkg/transport/router/pattern"

jErrors "github.com/juju/errors"
)
Expand All @@ -27,6 +27,17 @@ func NewRouter() *Router {
}
}

func NewRouterWithRoutes(route []*Route) (*Router, error) {
r := NewRouter()
for _, rt := range route {
err := r.Push(rt)
if err != nil {
return nil, jErrors.Trace(err)
}
}
return r, nil
}

type routeMatcher struct {
matcher *routePattern.Matcher
grpcSpec *GrpcSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package router_test
import (
"testing"

"github.com/eset/grpc-rest-proxy/pkg/transport/router"
"github.com/eset/grpc-rest-proxy/pkg/service/router"

"github.com/stretchr/testify/require"
"google.golang.org/genproto/googleapis/api/annotations"
Expand Down
Loading

0 comments on commit 9163358

Please sign in to comment.