From e4ba134fa6d431f687ad2c9bd7f3593a05617624 Mon Sep 17 00:00:00 2001 From: pugnack Date: Tue, 29 Apr 2025 20:34:11 +0500 Subject: [PATCH] [v4] breaking change: modify API for working with response metadata (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement functions to append/get metadata * сhanged behavior to return nil instead of empty metadata for getResponseMetadata() * removed metadata copy when passing to gRPC headers --------- Co-authored-by: Vasiliy Tolstov --- go.mod | 3 ++ grpc.go | 9 ++-- metadata.go | 47 ++++++++++++++++ metadata_test.go | 136 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 metadata.go create mode 100644 metadata_test.go diff --git a/go.mod b/go.mod index 4dfd080..a0d15e6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.24.2 require ( + github.com/stretchr/testify v1.10.0 go.unistack.org/micro/v4 v4.1.8 golang.org/x/net v0.39.0 google.golang.org/grpc v1.72.0 @@ -13,8 +14,10 @@ require ( require ( github.com/ash3in/uuidv8 v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/google/uuid v1.6.0 // indirect github.com/matoous/go-nanoid v1.5.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cast v1.7.1 // indirect go.unistack.org/micro-proto/v4 v4.1.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/grpc.go b/grpc.go index 4e3bb32..9c4b3a7 100644 --- a/grpc.go +++ b/grpc.go @@ -263,6 +263,7 @@ func (g *Server) handler(srv interface{}, stream grpc.ServerStream) error { // create new context ctx = metadata.NewIncomingContext(ctx, md) ctx = metadata.NewOutgoingContext(ctx, metadata.New(0)) + ctx = context.WithValue(ctx, rspMetadataKey{}, &rspMetadataVal{}) stream = &streamWrapper{ctx, stream} @@ -397,8 +398,8 @@ func (g *Server) processRequest(ctx context.Context, stream grpc.ServerStream, s statusDesc := "" // execute the handler appErr := fn(ctx, r, replyv.Interface()) - if outmd, ok := metadata.FromOutgoingContext(ctx); ok { - if err = stream.SendHeader(gmetadata.MD(outmd.Copy())); err != nil { + if md := getResponseMetadata(ctx); len(md) > 0 { + if err = stream.SendHeader(gmetadata.MD(md)); err != nil { return err } } @@ -481,8 +482,8 @@ func (g *Server) processStream(ctx context.Context, stream grpc.ServerStream, se statusDesc := "" appErr := fn(ctx, r, ss) - if outmd, ok := metadata.FromOutgoingContext(ctx); ok { - if err := stream.SendHeader(gmetadata.MD(outmd.Copy())); err != nil { + if md := getResponseMetadata(ctx); len(md) > 0 { + if err := stream.SendHeader(gmetadata.MD(md)); err != nil { return err } } diff --git a/metadata.go b/metadata.go new file mode 100644 index 0000000..962cad5 --- /dev/null +++ b/metadata.go @@ -0,0 +1,47 @@ +package grpc + +import ( + "context" + + "go.unistack.org/micro/v4/metadata" +) + +type ( + rspMetadataKey struct{} + rspMetadataVal struct { + m metadata.Metadata + } +) + +// AppendResponseMetadata adds metadata entries to metadata.Metadata stored in the context. +// It expects the context to contain a *rspMetadataVal value under the rspMetadataKey{} key. +// If the value is missing or invalid, the function does nothing. +// +// Note: this function is not thread-safe. Synchronization is required if used from multiple goroutines. +func AppendResponseMetadata(ctx context.Context, md metadata.Metadata) { + if md == nil { + return + } + + val, ok := ctx.Value(rspMetadataKey{}).(*rspMetadataVal) + if !ok || val == nil || val.m == nil { + return + } + + for key, values := range md { + val.m.Append(key, values...) + } +} + +// getResponseMetadata retrieves the metadata.Metadata stored in the context. +// +// Note: this function is not thread-safe. Synchronization is required if used from multiple goroutines. +// If you plan to modify the returned metadata, make a full copy to avoid affecting shared state. +func getResponseMetadata(ctx context.Context) metadata.Metadata { + val, ok := ctx.Value(rspMetadataKey{}).(*rspMetadataVal) + if !ok || val == nil || val.m == nil { + return nil + } + + return val.m +} diff --git a/metadata_test.go b/metadata_test.go new file mode 100644 index 0000000..cb7ccec --- /dev/null +++ b/metadata_test.go @@ -0,0 +1,136 @@ +package grpc + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.unistack.org/micro/v4/metadata" +) + +func TestAppendResponseMetadata(t *testing.T) { + tests := []struct { + name string + ctx context.Context + md metadata.Metadata + expected context.Context + }{ + { + name: "nil metadata", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + md: nil, + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + }, + { + name: "empty metadata", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + md: metadata.Metadata{}, + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + }, + { + name: "context without response metadata key", + ctx: context.Background(), + md: metadata.Pairs("key1", "val1"), + expected: context.Background(), + }, + { + name: "context with nil response metadata value", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, nil), + md: metadata.Pairs("key1", "val1"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, nil), + }, + { + name: "context with incorrect type in response metadata value", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, struct{}{}), + md: metadata.Pairs("key1", "val1"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, struct{}{}), + }, + { + name: "context with response metadata value, but nil metadata", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), + md: metadata.Pairs("key1", "val1"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), + }, + { + name: "basic metadata append", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + md: metadata.Pairs("key1", "val1"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ + m: metadata.Metadata{ + "key1": []string{"val1"}, + }, + }), + }, + { + name: "multiple values for same key", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + md: metadata.Pairs("key1", "val1", "key1", "val2"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ + m: metadata.Metadata{ + "key1": []string{"val1", "val2"}, + }, + }), + }, + { + name: "multiple values for different keys", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: metadata.Metadata{}}), + md: metadata.Pairs("key1", "val1", "key1", "val2", "key2", "val3", "key2", "val4", "key3", "val5"), + expected: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ + m: metadata.Metadata{ + "key1": []string{"val1", "val2"}, + "key2": []string{"val3", "val4"}, + "key3": []string{"val5"}, + }, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AppendResponseMetadata(tt.ctx, tt.md) + require.Equal(t, tt.expected, tt.ctx) + }) + } +} + +func TestGetResponseMetadata(t *testing.T) { + tests := []struct { + name string + ctx context.Context + expected metadata.Metadata + }{ + { + name: "context without response metadata key", + ctx: context.Background(), + expected: nil, + }, + { + name: "context with nil response metadata value", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, nil), + expected: nil, + }, + { + name: "context with incorrect type in response metadata value", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &struct{}{}), + expected: nil, + }, + { + name: "context with response metadata value, but nil metadata", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{m: nil}), + expected: nil, + }, + { + name: "valid metadata", + ctx: context.WithValue(context.Background(), rspMetadataKey{}, &rspMetadataVal{ + m: metadata.Pairs("key1", "value1"), + }), + expected: metadata.Metadata{"key1": {"value1"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expected, getResponseMetadata(tt.ctx)) + }) + } +}