From 916335843f7994d8fcef969d5e2125064a5c9416 Mon Sep 17 00:00:00 2001 From: Martin Kuzma Date: Wed, 21 Aug 2024 14:47:34 +0200 Subject: [PATCH] Add error response and support for serialising custom messages (#8) * Implement error response with custom details * Create separate protobuf message for error status. * Remove unused attribute --- Makefile | 3 + README.md | 19 ++ buf.gen.yaml | 11 + buf.yaml | 11 + .../grpcserver/gen/user/v1/user.pb.go | 238 ++++++++++++------ .../grpcserver/proto/user/v1/user.proto | 5 + cmd/examples/grpcserver/userservice.go | 15 +- cmd/service/app.go | 49 ++-- pkg/service/jsonencoder/encoder.go | 11 +- pkg/service/protoparser/parser.go | 56 ++++- pkg/service/protoparser/result.go | 3 +- .../router/grpc_spec.go | 0 pkg/{transport => service}/router/method.go | 0 .../router/pattern/iterator.go | 0 .../router/pattern/iterator_parser.go | 0 .../router/pattern/matcher.go | 0 .../router/pattern/parser.go | 0 .../router/pattern/pattern.go | 0 .../router/pattern/pattern_test.go | 2 +- pkg/{transport => service}/router/route.go | 0 pkg/{transport => service}/router/router.go | 13 +- .../router/router_test.go | 2 +- pkg/transport/endpoint.go | 148 +++++++++++ pkg/transport/endpoint_reloader.go | 38 +++ pkg/transport/router/wrapper.go | 32 --- pkg/transport/status/error.pb.go | 181 +++++++++++++ pkg/transport/status/status.go | 37 +++ pkg/transport/transport.go | 116 +-------- protos/transport/status/error.proto | 20 ++ 29 files changed, 749 insertions(+), 261 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml rename pkg/{transport => service}/router/grpc_spec.go (100%) rename pkg/{transport => service}/router/method.go (100%) rename pkg/{transport => service}/router/pattern/iterator.go (100%) rename pkg/{transport => service}/router/pattern/iterator_parser.go (100%) rename pkg/{transport => service}/router/pattern/matcher.go (100%) rename pkg/{transport => service}/router/pattern/parser.go (100%) rename pkg/{transport => service}/router/pattern/pattern.go (100%) rename pkg/{transport => service}/router/pattern/pattern_test.go (97%) rename pkg/{transport => service}/router/route.go (100%) rename pkg/{transport => service}/router/router.go (88%) rename pkg/{transport => service}/router/router_test.go (97%) create mode 100644 pkg/transport/endpoint.go create mode 100644 pkg/transport/endpoint_reloader.go delete mode 100644 pkg/transport/router/wrapper.go create mode 100644 pkg/transport/status/error.pb.go create mode 100644 pkg/transport/status/status.go create mode 100644 protos/transport/status/error.proto diff --git a/Makefile b/Makefile index 38a3f98..f72f5ac 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 80d05f4..662eb07 100644 --- a/README.md +++ b/README.md @@ -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." + } + ] +} ``` \ No newline at end of file diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..1d228e0 --- /dev/null +++ b/buf.gen.yaml @@ -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 \ No newline at end of file diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..632e575 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,11 @@ +version: v2 +modules: + - path: protos +deps: + - buf.build/googleapis/googleapis +lint: + use: + - DEFAULT +breaking: + use: + - FILE \ No newline at end of file diff --git a/cmd/examples/grpcserver/gen/user/v1/user.pb.go b/cmd/examples/grpcserver/gen/user/v1/user.pb.go index 59400e7..55e54b9 100644 --- a/cmd/examples/grpcserver/gen/user/v1/user.pb.go +++ b/cmd/examples/grpcserver/gen/user/v1/user.pb.go @@ -1093,6 +1093,61 @@ func (x *Job) GetJobType() string { return "" } +type GetUserError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Recommendation string `protobuf:"bytes,2,opt,name=recommendation,proto3" json:"recommendation,omitempty"` +} + +func (x *GetUserError) Reset() { + *x = GetUserError{} + if protoimpl.UnsafeEnabled { + mi := &file_user_v1_user_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetUserError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserError) ProtoMessage() {} + +func (x *GetUserError) ProtoReflect() protoreflect.Message { + mi := &file_user_v1_user_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserError.ProtoReflect.Descriptor instead. +func (*GetUserError) Descriptor() ([]byte, []int) { + return file_user_v1_user_proto_rawDescGZIP(), []int{18} +} + +func (x *GetUserError) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GetUserError) GetRecommendation() string { + if x != nil { + return x.Recommendation + } + return "" +} + var File_user_v1_user_proto protoreflect.FileDescriptor var file_user_v1_user_proto_rawDesc = []byte{ @@ -1192,89 +1247,95 @@ var file_user_v1_user_proto_rawDesc = []byte{ 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x6f, 0x62, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x54, 0x79, 0x70, - 0x65, 0x2a, 0x56, 0x0a, 0x04, 0x50, 0x6f, 0x73, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x4f, - 0x44, 0x55, 0x43, 0x54, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x4e, 0x47, 0x41, 0x47, 0x45, - 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x4f, 0x4d, 0x4f, 0x54, - 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x4f, 0x4d, 0x50, 0x45, 0x54, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x45, 0x57, 0x53, 0x5f, 0x54, - 0x52, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x32, 0xb2, 0x08, 0x0a, 0x0b, 0x55, 0x73, - 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5d, 0x0a, 0x07, 0x47, 0x65, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, - 0x01, 0x2a, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x7b, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x88, 0x01, 0x0a, 0x08, 0x47, 0x65, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x42, 0x5a, 0x29, 0x12, 0x27, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, - 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x79, 0x2f, 0x7b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x7d, 0x12, 0x15, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, - 0x6d, 0x65, 0x7d, 0x12, 0x74, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x42, - 0x79, 0x4a, 0x6f, 0x62, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x65, 0x22, 0x52, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, + 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x56, 0x0a, 0x04, 0x50, 0x6f, 0x73, 0x74, 0x12, 0x0b, 0x0a, + 0x07, 0x50, 0x52, 0x4f, 0x44, 0x55, 0x43, 0x54, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x4e, + 0x47, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, + 0x4f, 0x4d, 0x4f, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x43, 0x4f, 0x4d, + 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x4e, 0x45, + 0x57, 0x53, 0x5f, 0x54, 0x52, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x32, 0xb2, 0x08, + 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5d, 0x0a, + 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, + 0x93, 0x02, 0x19, 0x3a, 0x01, 0x2a, 0x12, 0x14, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, + 0x72, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x88, 0x01, 0x0a, + 0x08, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x74, - 0x69, 0x74, 0x6c, 0x65, 0x3d, 0x2f, 0x2a, 0x2f, 0x7d, 0x12, 0x5f, 0x0a, 0x0b, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, - 0x65, 0x72, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x8b, 0x01, 0x0a, 0x0c, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x50, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x73, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3a, 0x12, 0x38, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x2f, 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x72, 0x79, 0x3d, 0x2f, 0x2a, 0x2f, 0x7d, 0x2f, 0x70, 0x6f, 0x73, 0x74, 0x73, 0x2f, 0x7b, - 0x74, 0x79, 0x70, 0x65, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x73, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1a, 0x2e, 0x75, 0x73, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x42, 0x5a, 0x29, 0x12, 0x27, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x2f, 0x7b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x7d, + 0x12, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x7b, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x12, 0x74, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x73, 0x42, 0x79, 0x4a, 0x6f, 0x62, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x17, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x2a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x24, 0x12, 0x22, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x6a, 0x6f, 0x62, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x2e, 0x6a, + 0x6f, 0x62, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3d, 0x2f, 0x2a, 0x2f, 0x7d, 0x12, 0x5f, 0x0a, + 0x0b, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x8b, + 0x01, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x50, 0x6f, 0x73, 0x74, 0x12, + 0x1b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x40, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x3a, 0x12, 0x38, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x7b, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2e, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x3d, 0x2f, 0x2a, 0x2f, 0x7d, 0x2f, 0x70, 0x6f, 0x73, + 0x74, 0x73, 0x2f, 0x7b, 0x74, 0x79, 0x70, 0x65, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x73, 0x0a, 0x0f, + 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, + 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x12, 0x1f, 0x2f, 0x61, - 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x2f, 0x7b, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x3d, 0x2a, 0x2a, 0x7d, 0x12, 0x63, 0x0a, - 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, - 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x12, 0x6e, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x12, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x21, 0x2a, 0x1f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x2f, 0x2a, - 0x2f, 0x7d, 0x12, 0x89, 0x01, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x4a, 0x6f, 0x62, 0x12, 0x1d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x39, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x33, 0x1a, 0x31, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x2f, - 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x7b, 0x6a, 0x6f, 0x62, 0x2e, - 0x6a, 0x6f, 0x62, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3d, 0x2a, 0x7d, 0x2f, 0x2a, 0x42, 0x94, - 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, - 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x73, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70, - 0x63, 0x2d, 0x72, 0x65, 0x73, 0x74, 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x63, 0x6d, 0x64, - 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, - 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x73, 0x65, - 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x73, 0x65, - 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x21, + 0x12, 0x1f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x2f, 0x7b, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x3d, 0x2a, 0x2a, + 0x7d, 0x12, 0x63, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x3a, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x22, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2f, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x6e, 0x0a, 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x27, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x21, 0x2a, 0x1f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, + 0x2f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x3d, 0x2f, 0x2a, 0x2f, 0x7d, 0x12, 0x89, 0x01, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x62, 0x12, 0x1d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x62, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, + 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x62, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x39, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x33, 0x1a, + 0x31, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x2f, 0x7b, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x7b, + 0x6a, 0x6f, 0x62, 0x2e, 0x6a, 0x6f, 0x62, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x3d, 0x2a, 0x7d, + 0x2f, 0x2a, 0x42, 0x94, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x73, 0x65, 0x74, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x72, 0x65, 0x73, 0x74, 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x67, 0x72, + 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, + 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, + 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c, + 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -1290,7 +1351,7 @@ func file_user_v1_user_proto_rawDescGZIP() []byte { } var file_user_v1_user_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_user_v1_user_proto_goTypes = []any{ (Post)(0), // 0: user.v1.Post (*GetUserPostRequest)(nil), // 1: user.v1.GetUserPostRequest @@ -1311,6 +1372,7 @@ var file_user_v1_user_proto_goTypes = []any{ (*User)(nil), // 16: user.v1.User (*Address)(nil), // 17: user.v1.Address (*Job)(nil), // 18: user.v1.Job + (*GetUserError)(nil), // 19: user.v1.GetUserError } var file_user_v1_user_proto_depIdxs = []int32{ 17, // 0: user.v1.GetUserPostRequest.address:type_name -> user.v1.Address @@ -1572,6 +1634,18 @@ func file_user_v1_user_proto_init() { return nil } } + file_user_v1_user_proto_msgTypes[18].Exporter = func(v any, i int) any { + switch v := v.(*GetUserError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1579,7 +1653,7 @@ func file_user_v1_user_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_user_v1_user_proto_rawDesc, NumEnums: 1, - NumMessages: 18, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/cmd/examples/grpcserver/proto/user/v1/user.proto b/cmd/examples/grpcserver/proto/user/v1/user.proto index d44ca9a..617ec30 100644 --- a/cmd/examples/grpcserver/proto/user/v1/user.proto +++ b/cmd/examples/grpcserver/proto/user/v1/user.proto @@ -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; } \ No newline at end of file diff --git a/cmd/examples/grpcserver/userservice.go b/cmd/examples/grpcserver/userservice.go index 5bcc14a..b709aa6 100644 --- a/cmd/examples/grpcserver/userservice.go +++ b/cmd/examples/grpcserver/userservice.go @@ -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 { @@ -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) { diff --git a/cmd/service/app.go b/cmd/service/app.go index b649e33..3ca5eb8 100644 --- a/cmd/service/app.go +++ b/cmd/service/app.go @@ -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 { @@ -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 } @@ -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") @@ -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) { @@ -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))) } diff --git a/pkg/service/jsonencoder/encoder.go b/pkg/service/jsonencoder/encoder.go index 00c8e22..a763243 100644 --- a/pkg/service/jsonencoder/encoder.go +++ b/pkg/service/jsonencoder/encoder.go @@ -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" ) @@ -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, + }, } } diff --git a/pkg/service/protoparser/parser.go b/pkg/service/protoparser/parser.go index 6657bd9..cc13ed8 100644 --- a/pkg/service/protoparser/parser.go +++ b/pkg/service/protoparser/parser.go @@ -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" @@ -16,6 +16,7 @@ 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" ) @@ -23,6 +24,7 @@ import ( func ParseFileDescSets(fdSets []*descriptorpb.FileDescriptorSet) ParseResult { result := ParseResult{ FileRegistry: &protoregistry.Files{}, + TypeResolver: &protoregistry.Types{}, } // Register default types. @@ -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) @@ -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 +} diff --git a/pkg/service/protoparser/result.go b/pkg/service/protoparser/result.go index aca113f..6d61a68 100644 --- a/pkg/service/protoparser/result.go +++ b/pkg/service/protoparser/result.go @@ -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 } diff --git a/pkg/transport/router/grpc_spec.go b/pkg/service/router/grpc_spec.go similarity index 100% rename from pkg/transport/router/grpc_spec.go rename to pkg/service/router/grpc_spec.go diff --git a/pkg/transport/router/method.go b/pkg/service/router/method.go similarity index 100% rename from pkg/transport/router/method.go rename to pkg/service/router/method.go diff --git a/pkg/transport/router/pattern/iterator.go b/pkg/service/router/pattern/iterator.go similarity index 100% rename from pkg/transport/router/pattern/iterator.go rename to pkg/service/router/pattern/iterator.go diff --git a/pkg/transport/router/pattern/iterator_parser.go b/pkg/service/router/pattern/iterator_parser.go similarity index 100% rename from pkg/transport/router/pattern/iterator_parser.go rename to pkg/service/router/pattern/iterator_parser.go diff --git a/pkg/transport/router/pattern/matcher.go b/pkg/service/router/pattern/matcher.go similarity index 100% rename from pkg/transport/router/pattern/matcher.go rename to pkg/service/router/pattern/matcher.go diff --git a/pkg/transport/router/pattern/parser.go b/pkg/service/router/pattern/parser.go similarity index 100% rename from pkg/transport/router/pattern/parser.go rename to pkg/service/router/pattern/parser.go diff --git a/pkg/transport/router/pattern/pattern.go b/pkg/service/router/pattern/pattern.go similarity index 100% rename from pkg/transport/router/pattern/pattern.go rename to pkg/service/router/pattern/pattern.go diff --git a/pkg/transport/router/pattern/pattern_test.go b/pkg/service/router/pattern/pattern_test.go similarity index 97% rename from pkg/transport/router/pattern/pattern_test.go rename to pkg/service/router/pattern/pattern_test.go index 9efa58e..c9e9b1a 100644 --- a/pkg/transport/router/pattern/pattern_test.go +++ b/pkg/service/router/pattern/pattern_test.go @@ -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" ) diff --git a/pkg/transport/router/route.go b/pkg/service/router/route.go similarity index 100% rename from pkg/transport/router/route.go rename to pkg/service/router/route.go diff --git a/pkg/transport/router/router.go b/pkg/service/router/router.go similarity index 88% rename from pkg/transport/router/router.go rename to pkg/service/router/router.go index 7150fa0..b922d13 100644 --- a/pkg/transport/router/router.go +++ b/pkg/service/router/router.go @@ -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" ) @@ -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 diff --git a/pkg/transport/router/router_test.go b/pkg/service/router/router_test.go similarity index 97% rename from pkg/transport/router/router_test.go rename to pkg/service/router/router_test.go index 997f237..85d8b3f 100644 --- a/pkg/transport/router/router_test.go +++ b/pkg/service/router/router_test.go @@ -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" diff --git a/pkg/transport/endpoint.go b/pkg/transport/endpoint.go new file mode 100644 index 0000000..a488ac0 --- /dev/null +++ b/pkg/transport/endpoint.go @@ -0,0 +1,148 @@ +// Copyright (c) 2024 ESET +// See LICENSE file for redistribution. +package transport + +import ( + "context" + "io" + "net/http" + "net/url" + "strings" + + grpcClient "github.com/eset/grpc-rest-proxy/pkg/gateway/grpc" + "github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder" + routerPkg "github.com/eset/grpc-rest-proxy/pkg/service/router" + "github.com/eset/grpc-rest-proxy/pkg/service/transformer" + statusPkg "github.com/eset/grpc-rest-proxy/pkg/transport/status" + + jErrors "github.com/juju/errors" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + grpcStatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/dynamicpb" +) + +type ProxyEndpoint struct { + logger Logger + router *routerPkg.Router + client grpcClient.ClientInterface + jsonEncoder jsonencoder.Encoder +} + +func NewProxyEndpoint( + logger Logger, + router *routerPkg.Router, + client grpcClient.ClientInterface, + jsonEncoder jsonencoder.Encoder, +) *ProxyEndpoint { + return &ProxyEndpoint{ + logger: logger, + router: router, + client: client, + jsonEncoder: jsonEncoder, + } +} + +func (e *ProxyEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) { + method, err := routerPkg.StringToMethod(r.Method) + if err != nil { + e.respondWithError(r.Context(), w, statusPkg.FromHTTPCode(http.StatusMethodNotAllowed)) + return + } + + routeMatch := e.router.Find(method, r.URL.Path) + if routeMatch == nil { + e.respondWithError(r.Context(), w, statusPkg.FromHTTPCode(http.StatusNotFound)) + return + } + + rpcRequest, err := convertRequestToGRPC(routeMatch, r) + if err != nil { + e.logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) + e.respondWithError(r.Context(), w, statusPkg.FromHTTPCode(http.StatusBadRequest)) + return + } + rpcResponse := transformer.GetRPCResponse(routeMatch.GrpcSpec.ResponseDesc) + + var header, trailer metadata.MD + err = e.client.Invoke( + transformer.GetRPCRequestContext(r), + routeMatch.GrpcSpec.FullPath(), + rpcRequest, + rpcResponse, + grpc.Header(&header), + grpc.Trailer(&trailer), + ) + if err != nil { + if errStatus, ok := grpcStatus.FromError(err); ok { + transformer.SetRESTHeaders(r.ProtoMajor, w.Header(), header, trailer) + e.respondWithError(r.Context(), w, statusPkg.FromGRPC(errStatus)) + return + } + e.logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) + e.respondWithError(r.Context(), w, statusPkg.FromHTTPCode(http.StatusInternalServerError)) + return + } + + transformer.SetRESTHeaders(r.ProtoMajor, w.Header(), header, trailer) + + response, err := e.jsonEncoder.Encode(rpcResponse) + if err != nil { + e.logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) + w.WriteHeader(http.StatusInternalServerError) + return + } + + _, err = w.Write(response) + if err != nil { + e.logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (e *ProxyEndpoint) respondWithError(ctx context.Context, w http.ResponseWriter, status *statusPkg.Error) { + encodedStatus, err := e.jsonEncoder.Encode(status) + if err != nil { + e.logger.ErrorContext(ctx, jErrors.Details(jErrors.Trace(err))) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(int(status.GetCode())) + + _, err = w.Write(encodedStatus) + if err != nil { + e.logger.ErrorContext(ctx, jErrors.Details(jErrors.Trace(err))) + } +} + +func getQueryVariables(queryValues url.Values) []transformer.Variable { + var queryVariables []transformer.Variable + for name, values := range queryValues { + fieldPath := strings.Split(name, ".") + for _, value := range values { + queryVariables = append(queryVariables, transformer.Variable{FieldPath: fieldPath, Value: value}) + } + } + return queryVariables +} + +func convertRequestToGRPC(route *routerPkg.Match, r *http.Request) (req *dynamicpb.Message, err error) { + reqBody, err := io.ReadAll(r.Body) + if err != nil { + return nil, jErrors.Trace(err) + } + r.Body.Close() + + queryVariables := getQueryVariables(r.URL.Query()) + route.Params = append(route.Params, queryVariables...) + + req, err = transformer.GetRPCRequest(reqBody, route.GrpcSpec.RequestDesc, route.Params, route.BodyRule) + if err != nil { + return nil, jErrors.Trace(err) + } + + return req, nil +} diff --git a/pkg/transport/endpoint_reloader.go b/pkg/transport/endpoint_reloader.go new file mode 100644 index 0000000..8bf66d6 --- /dev/null +++ b/pkg/transport/endpoint_reloader.go @@ -0,0 +1,38 @@ +// Copyright (c) 2024 ESET +// See LICENSE file for redistribution. +package transport + +import ( + "net/http" + "sync" +) + +// EndpointReloader is wrapper for Handle method to allow dynamic endpoint reloading +type EndpointReloader struct { + mtx sync.RWMutex + handler http.Handler +} + +func NewEndpointReloader(handler http.Handler) *EndpointReloader { + return &EndpointReloader{ + handler: handler, + } +} + +func (e *EndpointReloader) ServeHTTP(w http.ResponseWriter, r *http.Request) { + e.mtx.RLock() + defer e.mtx.RUnlock() + e.handler.ServeHTTP(w, r) +} + +func (e *EndpointReloader) Set(endpoint *ProxyEndpoint) { + e.mtx.Lock() + e.handler = endpoint + e.mtx.Unlock() +} + +func (e *EndpointReloader) Endpoint() http.Handler { + e.mtx.RLock() + defer e.mtx.RUnlock() + return e.handler +} diff --git a/pkg/transport/router/wrapper.go b/pkg/transport/router/wrapper.go deleted file mode 100644 index 53d4e2d..0000000 --- a/pkg/transport/router/wrapper.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2024 ESET -// See LICENSE file for redistribution. - -package router - -import ( - "sync" -) - -type ReloadableRouter struct { - router *Router - mtx sync.RWMutex -} - -func WithWrapper(router *Router) *ReloadableRouter { - return &ReloadableRouter{ - router: router, - mtx: sync.RWMutex{}, - } -} - -func (w *ReloadableRouter) Find(method MethodType, path string) (result *Match) { - w.mtx.RLock() - defer w.mtx.RUnlock() - return w.router.Find(method, path) -} - -func (w *ReloadableRouter) SetRouter(router *Router) { - w.mtx.Lock() - w.router = router - w.mtx.Unlock() -} diff --git a/pkg/transport/status/error.pb.go b/pkg/transport/status/error.pb.go new file mode 100644 index 0000000..b01d3b3 --- /dev/null +++ b/pkg/transport/status/error.pb.go @@ -0,0 +1,181 @@ +// Copyright (c) 2024 ESET +// See LICENSE file for redistribution. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc (unknown) +// source: transport/status/error.proto + +package status + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Error struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // HTTP status code. Corresponds to the HTTP status code returned by the backend server. + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + // HTTP status message or gRPC status message. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Additional details about the error. + Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"` +} + +func (x *Error) Reset() { + *x = Error{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_status_error_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Error) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Error) ProtoMessage() {} + +func (x *Error) ProtoReflect() protoreflect.Message { + mi := &file_transport_status_error_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Error.ProtoReflect.Descriptor instead. +func (*Error) Descriptor() ([]byte, []int) { + return file_transport_status_error_proto_rawDescGZIP(), []int{0} +} + +func (x *Error) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Error) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Error) GetDetails() []*anypb.Any { + if x != nil { + return x.Details + } + return nil +} + +var File_transport_status_error_proto protoreflect.FileDescriptor + +var file_transport_status_error_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x65, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, + 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, + 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x86, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, + 0x2e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x0a, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x65, 0x73, 0x65, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x72, 0x65, 0x73, 0x74, + 0x2d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0xa2, 0x02, 0x03, 0x53, 0x58, + 0x58, 0xaa, 0x02, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0xca, 0x02, 0x06, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0xe2, 0x02, 0x12, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_status_error_proto_rawDescOnce sync.Once + file_transport_status_error_proto_rawDescData = file_transport_status_error_proto_rawDesc +) + +func file_transport_status_error_proto_rawDescGZIP() []byte { + file_transport_status_error_proto_rawDescOnce.Do(func() { + file_transport_status_error_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_status_error_proto_rawDescData) + }) + return file_transport_status_error_proto_rawDescData +} + +var file_transport_status_error_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_status_error_proto_goTypes = []any{ + (*Error)(nil), // 0: status.Error + (*anypb.Any)(nil), // 1: google.protobuf.Any +} +var file_transport_status_error_proto_depIdxs = []int32{ + 1, // 0: status.Error.details:type_name -> google.protobuf.Any + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_status_error_proto_init() } +func file_transport_status_error_proto_init() { + if File_transport_status_error_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_status_error_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Error); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_transport_status_error_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_status_error_proto_goTypes, + DependencyIndexes: file_transport_status_error_proto_depIdxs, + MessageInfos: file_transport_status_error_proto_msgTypes, + }.Build() + File_transport_status_error_proto = out.File + file_transport_status_error_proto_rawDesc = nil + file_transport_status_error_proto_goTypes = nil + file_transport_status_error_proto_depIdxs = nil +} diff --git a/pkg/transport/status/status.go b/pkg/transport/status/status.go new file mode 100644 index 0000000..4ff0d3e --- /dev/null +++ b/pkg/transport/status/status.go @@ -0,0 +1,37 @@ +package status + +import ( + "net/http" + + "github.com/eset/grpc-rest-proxy/pkg/service/transformer" + + grpcStatus "google.golang.org/grpc/status" + anypb "google.golang.org/protobuf/types/known/anypb" +) + +func FromHTTPCode(code int) *Error { + return &Error{ + Code: int32(code), + Message: http.StatusText(code), + } +} + +func FromGRPC(status *grpcStatus.Status) *Error { + httpStatus := transformer.GetHTTPStatusCode(status.Code()) + + msg := status.Message() + if msg == "" { + msg = http.StatusText(httpStatus) + } + + var details []*anypb.Any + if status.Proto() != nil { + details = status.Proto().GetDetails() + } + + return &Error{ + Code: int32(httpStatus), + Message: msg, + Details: details, + } +} diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go index a7cc4f2..6ad7a85 100644 --- a/pkg/transport/transport.go +++ b/pkg/transport/transport.go @@ -8,133 +8,25 @@ import ( "fmt" "io" "net/http" - "net/url" - "strings" - - grpcClient "github.com/eset/grpc-rest-proxy/pkg/gateway/grpc" - "github.com/eset/grpc-rest-proxy/pkg/service/jsonencoder" - "github.com/eset/grpc-rest-proxy/pkg/service/transformer" - routerPkg "github.com/eset/grpc-rest-proxy/pkg/transport/router" logging "log/slog" "github.com/go-chi/chi/v5" - jErrors "github.com/juju/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/dynamicpb" ) -type Context struct { - Router *routerPkg.ReloadableRouter - GrcpClient grpcClient.ClientInterface - JSONEncoder jsonencoder.Encoder -} - type Logger interface { // Log error during request processing ErrorContext(ctx context.Context, msg string, args ...any) } -func NewHandler(routerContext *Context, logger Logger) http.Handler { +func NewHandler(reloader *EndpointReloader) http.Handler { routes := chi.NewRouter() - if logger == nil { - logger = logging.Default() - } - routes.HandleFunc("/*", createRoutingEndpoint(routerContext, logger)) - routes.Get("/status", statusJSON) + routes.Handle("/*", reloader) + routes.Get("/status", handleStatus) return routes } -func getQueryVariables(queryValues url.Values) []transformer.Variable { - var queryVariables []transformer.Variable - for name, values := range queryValues { - fieldPath := strings.Split(name, ".") - for _, value := range values { - queryVariables = append(queryVariables, transformer.Variable{FieldPath: fieldPath, Value: value}) - } - } - return queryVariables -} - -func convertRequestToGRPC(route *routerPkg.Match, r *http.Request) (req *dynamicpb.Message, err error) { - reqBody, err := io.ReadAll(r.Body) - if err != nil { - return nil, jErrors.Trace(err) - } - r.Body.Close() - - queryVariables := getQueryVariables(r.URL.Query()) - route.Params = append(route.Params, queryVariables...) - - req, err = transformer.GetRPCRequest(reqBody, route.GrpcSpec.RequestDesc, route.Params, route.BodyRule) - if err != nil { - return nil, jErrors.Trace(err) - } - - return req, nil -} - -func createRoutingEndpoint(rc *Context, logger Logger) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - method, err := routerPkg.StringToMethod(r.Method) - if err != nil { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - routeMatch := rc.Router.Find(method, r.URL.Path) - if routeMatch == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - rpcRequest, err := convertRequestToGRPC(routeMatch, r) - if err != nil { - logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) - w.WriteHeader(http.StatusBadRequest) - return - } - rpcResponse := transformer.GetRPCResponse(routeMatch.GrpcSpec.ResponseDesc) - - var header, trailer metadata.MD - err = rc.GrcpClient.Invoke( - transformer.GetRPCRequestContext(r), - routeMatch.GrpcSpec.FullPath(), - rpcRequest, - rpcResponse, - grpc.Header(&header), - grpc.Trailer(&trailer), - ) - if err != nil { - if e, ok := status.FromError(err); ok { - transformer.SetRESTHeaders(r.ProtoMajor, w.Header(), header, trailer) - w.WriteHeader(transformer.GetHTTPStatusCode(e.Code())) - } - logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) - return - } - - transformer.SetRESTHeaders(r.ProtoMajor, w.Header(), header, trailer) - - response, err := rc.JSONEncoder.Encode(rpcResponse) - if err != nil { - logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) - w.WriteHeader(http.StatusInternalServerError) - return - } - - _, err = w.Write(response) - if err != nil { - logger.ErrorContext(r.Context(), jErrors.Details(jErrors.Trace(err))) - w.WriteHeader(http.StatusInternalServerError) - return - } - } -} - -func statusJSON(w http.ResponseWriter, _ *http.Request) { +func handleStatus(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, err := io.WriteString(w, `{"status":"OK"}`) if err != nil { diff --git a/protos/transport/status/error.proto b/protos/transport/status/error.proto new file mode 100644 index 0000000..cd6dd06 --- /dev/null +++ b/protos/transport/status/error.proto @@ -0,0 +1,20 @@ +// Copyright (c) 2024 ESET +// See LICENSE file for redistribution. +syntax = "proto3"; + +package status; + +import "google/protobuf/any.proto"; + +option go_package = "github.com/eset/grpc-rest-proxy/pkg/transport/status"; + +message Error { + // HTTP status code. Corresponds to the HTTP status code returned by the backend server. + int32 code = 1; + + // User-facing readable HTTP status message or status message returned by backend server. + string message = 2; + + // Additional details about the error. + repeated google.protobuf.Any details = 3; +}