diff --git a/schema/google/showcase/v1beta1/echo.proto b/schema/google/showcase/v1beta1/echo.proto index 3f79b4457..101b37c25 100644 --- a/schema/google/showcase/v1beta1/echo.proto +++ b/schema/google/showcase/v1beta1/echo.proto @@ -19,6 +19,7 @@ import "google/api/client.proto"; import "google/api/field_behavior.proto"; import "google/api/routing.proto"; import "google/longrunning/operations.proto"; +import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "google/rpc/status.proto"; @@ -34,7 +35,7 @@ option ruby_package = "Google::Showcase::V1beta1"; // side streaming, client side streaming, and bidirectional streaming. This // service also exposes methods that explicitly implement server delay, and // paginated calls. Set the 'showcase-trailer' metadata key on any method -// to have the values echoed in the response trailers. Set the +// to have the values echoed in the response trailers. Set the // 'x-goog-request-params' metadata key on any method to have the values // echoed in the response headers. service Echo { @@ -83,6 +84,19 @@ service Echo { }; } + // This method returns error details in a repeated "google.protobuf.Any" + // field. This method showcases handling errors thus encoded, particularly + // over REST transport. Note that GAPICs only allow the type + // "google.protobuf.Any" for field paths ending in "error.details", and, at + // run-time, the actual types for these fields must be one of the types in + // google/rpc/error_details.proto. + rpc EchoErrorDetails(EchoErrorDetailsRequest) returns (EchoErrorDetailsResponse) { + option (google.api.http) = { + post: "/v1beta1/echo:error-details" + body: "*" + }; + } + // This method splits the given content into words and will pass each word back // through the stream. This method showcases server-side streaming RPCs. rpc Expand(ExpandRequest) returns (stream EchoResponse) { @@ -206,6 +220,40 @@ message EchoResponse { Severity severity = 2; } +// The request message used for the EchoErrorDetails method. +message EchoErrorDetailsRequest { + // Content to return in a singular `*.error.details` field of type + // `google.protobuf.Any` + string single_detail_text = 1; + + // Content to return in a repeated `*.error.details` field of type + // `google.protobuf.Any` + repeated string multi_detail_text = 2; +} + +// The response message used for the EchoErrorDetails method. +message EchoErrorDetailsResponse { + + message SingleDetail { + ErrorWithSingleDetail error = 1; + } + + message MultipleDetails { + ErrorWithMultipleDetails error = 1; + } + + SingleDetail single_detail = 1; + MultipleDetails multiple_details = 2; +} + +message ErrorWithSingleDetail { + google.protobuf.Any details = 1; +} + +message ErrorWithMultipleDetails { + repeated google.protobuf.Any details = 1; +} + // The request message for the Expand method. message ExpandRequest { // The content that will be split into words and returned on the stream. diff --git a/server/services/echo_service.go b/server/services/echo_service.go index a75e6fe0e..09ff7835e 100644 --- a/server/services/echo_service.go +++ b/server/services/echo_service.go @@ -16,6 +16,7 @@ package services import ( "context" + "fmt" "io" "strconv" "strings" @@ -26,10 +27,12 @@ import ( "github.com/googleapis/gapic-showcase/server" pb "github.com/googleapis/gapic-showcase/server/genproto" lropb "google.golang.org/genproto/googleapis/longrunning" + errdetails "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + anypb "google.golang.org/protobuf/types/known/anypb" ) // NewEchoServer returns a new EchoServer for the Showcase API. @@ -51,6 +54,50 @@ func (s *echoServerImpl) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.Echo return &pb.EchoResponse{Content: in.GetContent(), Severity: in.GetSeverity()}, nil } +func (s *echoServerImpl) EchoErrorDetails(ctx context.Context, in *pb.EchoErrorDetailsRequest) (*pb.EchoErrorDetailsResponse, error) { + var singleDetailError *pb.EchoErrorDetailsResponse_SingleDetail + singleDetailText := in.GetSingleDetailText() + if len(singleDetailText) > 0 { + singleErrorInfo := &errdetails.ErrorInfo{Reason: singleDetailText} + singleMarshalledError, err := anypb.New(singleErrorInfo) + if err != nil { + return nil, fmt.Errorf("failure with single error detail in EchoErrorDetails: %w", err) + } + singleDetailError = &pb.EchoErrorDetailsResponse_SingleDetail{ + Error: &pb.ErrorWithSingleDetail{Details: singleMarshalledError}, + } + } + + var multipleDetailsError *pb.EchoErrorDetailsResponse_MultipleDetails + multipleDetailText := in.GetMultiDetailText() + if len(multipleDetailText) > 0 { + details := []*anypb.Any{} + for idx, text := range multipleDetailText { + errorInfo := &errdetails.ErrorInfo{ + Reason: text, + } + marshalledError, err := anypb.New(errorInfo) + if err != nil { + return nil, fmt.Errorf("failure in EchoErrorDetails[%d]: %w", idx, err) + } + + details = append(details, marshalledError) + } + + multipleDetailsError = &pb.EchoErrorDetailsResponse_MultipleDetails{ + Error: &pb.ErrorWithMultipleDetails{Details: details}, + } + } + + echoHeaders(ctx) + echoTrailers(ctx) + response := &pb.EchoErrorDetailsResponse{ + SingleDetail: singleDetailError, + MultipleDetails: multipleDetailsError, + } + return response, nil +} + func (s *echoServerImpl) Expand(in *pb.ExpandRequest, stream pb.Echo_ExpandServer) error { for _, word := range strings.Fields(in.GetContent()) { err := stream.Send(&pb.EchoResponse{Content: word}) diff --git a/server/services/echo_service_test.go b/server/services/echo_service_test.go index 48340b120..98affdff8 100644 --- a/server/services/echo_service_test.go +++ b/server/services/echo_service_test.go @@ -27,6 +27,7 @@ import ( "github.com/golang/protobuf/ptypes" durpb "github.com/golang/protobuf/ptypes/duration" pb "github.com/googleapis/gapic-showcase/server/genproto" + "google.golang.org/genproto/googleapis/rpc/errdetails" spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -402,6 +403,122 @@ func TestChat(t *testing.T) { } } +func TestEchoErrorDetails_single(t *testing.T) { + tests := []struct { + text string + expected *errdetails.ErrorInfo + }{ + {"Spanish rain", &errdetails.ErrorInfo{Reason: "Spanish rain"}}, + {"", &errdetails.ErrorInfo{Reason: ""}}, + } + + server := NewEchoServer() + for idx, test := range tests { + request := &pb.EchoErrorDetailsRequest{SingleDetailText: test.text} + out, err := server.EchoErrorDetails(context.Background(), request) + if err != nil { + t.Errorf("[%d] error calling EchoErrorSingleDetail(): %v", idx, err) + continue + } + if out.MultipleDetails != nil { + t.Errorf("[%d] expected no MultipleDetails, but got: %#v", idx, out.MultipleDetails) + } + if len(test.text) == 0 { + if out.SingleDetail != nil { + t.Errorf("[%d] expected no SingleDetail, but got: %#v", idx, out.SingleDetail) + } + continue + } + if out.SingleDetail == nil { + t.Errorf("[%d] no SingleDetail returned", idx) + continue + } + if out.SingleDetail.Error == nil { + t.Errorf("[%d] no SingleDetail.Error returned", idx) + continue + } + if out.SingleDetail.Error.Details == nil { + t.Errorf("[%d] no SingleDetail.Error.Details returned", idx) + continue + } + if got, want := out.SingleDetail.Error.Details.TypeUrl, "type.googleapis.com/google.rpc.ErrorInfo"; got != want { + t.Errorf("[%d] expected type URL %q; got %q ", idx, want, got) + } + unmarshalledError := &errdetails.ErrorInfo{} + if err := out.SingleDetail.Error.Details.UnmarshalTo(unmarshalledError); err != nil { + t.Errorf("[%d] error unmarshalling to ErrorInfo: %v", idx, err) + } + if got, want := unmarshalledError, test.expected; !proto.Equal(got, want) { + t.Errorf("[%d] expected ErrorInfo %v; got %v ", idx, want, got) + } + } +} + +func TestEchoErrorDetails_multiple(t *testing.T) { + tests := []struct { + text []string + expected []*errdetails.ErrorInfo + }{ + { + []string{"rain", "snow", "hail", "sleet", "fog"}, + []*errdetails.ErrorInfo{ + {Reason: "rain"}, + {Reason: "snow"}, + {Reason: "hail"}, + {Reason: "sleet"}, + {Reason: "fog"}, + }, + }, + {nil, nil}, + } + + server := NewEchoServer() + for idx, test := range tests { + request := &pb.EchoErrorDetailsRequest{MultiDetailText: test.text} + out, err := server.EchoErrorDetails(context.Background(), request) + if err != nil { + t.Errorf("[%d] error calling EchoErrorDetails(): %v", idx, err) + continue + } + if out.SingleDetail != nil { + t.Errorf("[%d] expected no SingleDetail, but got: %#v", idx, out.SingleDetail) + } + if len(test.text) == 0 { + if out.MultipleDetails != nil { + t.Errorf("[%d] expected no MultipleDetails, but got %#v", idx, out.MultipleDetails) + } + continue + } + if out.MultipleDetails == nil { + t.Errorf("[%d] no MultipleDetails returned", idx) + continue + } + if out.MultipleDetails.Error == nil { + t.Errorf("[%d] no MultipleDetails.Error returned", idx) + continue + } + if out.MultipleDetails.Error.Details == nil { + t.Errorf("[%d] no MultipleDetails.Error.Details returned", idx) + continue + } + if got, want := len(out.MultipleDetails.Error.Details), len(test.expected); got != want { + t.Errorf("[%d] expected %d MultipleDetails.Error.Details, got %d", idx, want, got) + } + for whichDetail, detail := range out.MultipleDetails.Error.Details { + if got, want := detail.TypeUrl, "type.googleapis.com/google.rpc.ErrorInfo"; got != want { + t.Errorf("[%d:%d] expected type URL %q; got %q ", idx, whichDetail, want, got) + } + unmarshalledError := &errdetails.ErrorInfo{} + if err := detail.UnmarshalTo(unmarshalledError); err != nil { + t.Errorf("[%d:%d] error unmarshalling to ErrorInfo: %v", idx, whichDetail, err) + } + if got, want := unmarshalledError, test.expected[whichDetail]; !proto.Equal(got, want) { + t.Errorf("[%d:%d] expected ErrorInfo %v; got %v ", idx, whichDetail, want, got) + } + } + } +} + type errorChatStream struct { err error pb.Echo_ChatServer