diff --git a/internal/transport/controlbuf.go b/internal/transport/controlbuf.go index 2dcd1e63bdd2..35ceb1928102 100644 --- a/internal/transport/controlbuf.go +++ b/internal/transport/controlbuf.go @@ -32,8 +32,11 @@ import ( "golang.org/x/net/http2/hpack" "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/internal/grpcutil" + "google.golang.org/grpc/internal/pretty" + istatus "google.golang.org/grpc/internal/status" "google.golang.org/grpc/mem" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) var updateHeaderTblSize = func(e *hpack.Encoder, v uint32) { @@ -854,6 +857,15 @@ func (l *loopyWriter) earlyAbortStreamHandler(eas *earlyAbortStream) error { {Name: "grpc-message", Value: encodeGrpcMessage(eas.status.Message())}, } + if p := istatus.RawStatusProto(eas.status); len(p.GetDetails()) > 0 { + stBytes, err := proto.Marshal(p) + if err != nil { + l.logger.Errorf("Failed to marshal rpc status: %s, error: %v", pretty.ToJSON(p), err) + } else { + headerFields = append(headerFields, hpack.HeaderField{Name: grpcStatusDetailsBinHeader, Value: encodeBinHeader(stBytes)}) + } + } + if err := l.writeHeader(eas.streamID, true, headerFields, nil); err != nil { return err } diff --git a/test/end2end_test.go b/test/end2end_test.go index 9157c525c094..037ab1a8db83 100644 --- a/test/end2end_test.go +++ b/test/end2end_test.go @@ -2154,6 +2154,50 @@ func testTap(t *testing.T, e env) { } } +func (s) TestTapStatusDetails(t *testing.T) { + tapHandler := func(context.Context, *tap.Info) (context.Context, error) { + // Return error with details for all RPCs. + wantDetails := &testpb.Empty{} + st := status.New(codes.ResourceExhausted, "rate limit exceeded") + st, err := st.WithDetails(wantDetails) + if err != nil { + t.Errorf("status.WithDetails() failed: %v", err) + } + return nil, st.Err() + } + + ss := stubserver.StartTestService(t, nil, grpc.InTapHandle(tapHandler)) + defer ss.Stop() + + if err := ss.StartClient(); err != nil { + t.Fatalf("ss.StartClient() failed: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() + + _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}) + if err == nil { + t.Fatal("EmptyCall() succeeded; want error") + } + + gotStatus := status.Convert(err) + if gotStatus.Code() != codes.ResourceExhausted { + t.Errorf("EmptyCall() returned code %v; want %v", gotStatus.Code(), codes.ResourceExhausted) + } + if gotStatus.Message() != "rate limit exceeded" { + t.Errorf("EmptyCall() returned message %q; want %q", gotStatus.Message(), "rate limit exceeded") + } + + details := gotStatus.Details() + if len(details) != 1 { + t.Fatalf("EmptyCall() returned %d details; want 1", len(details)) + } + if _, ok := details[0].(*testpb.Empty); !ok { + t.Fatalf("EmptyCall() returned detail type %T; want *testpb.Empty", details[0]) + } +} + func (s) TestEmptyUnaryWithUserAgent(t *testing.T) { for _, e := range listTestEnv() { testEmptyUnaryWithUserAgent(t, e)