Skip to content

Commit accd822

Browse files
committed
fix: preserve existing context metadata
1 parent e1baebf commit accd822

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,15 @@ func (d *DataSource) Load(ctx context.Context, headers http.Header, input []byte
117117

118118
// convert headers to grpc metadata and attach to ctx
119119
if len(headers) > 0 {
120-
md := make(metadata.MD, len(headers))
121-
for k, v := range headers {
122-
md.Set(strings.ToLower(k), v...)
120+
// assume that each header has exactly one value for default pairs size
121+
pairs := make([]string, 0, len(headers)*2)
122+
for headerName, headerValues := range headers {
123+
headerName = strings.ToLower(headerName)
124+
for _, v := range headerValues {
125+
pairs = append(pairs, headerName, v)
126+
}
123127
}
124-
ctx = metadata.NewOutgoingContext(ctx, md)
128+
ctx = metadata.AppendToOutgoingContext(ctx, pairs...)
125129
}
126130

127131
graph := NewDependencyGraph(d.plan)

v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/tidwall/gjson"
1515
"google.golang.org/grpc"
1616
"google.golang.org/grpc/credentials/insecure"
17+
"google.golang.org/grpc/metadata"
1718
"google.golang.org/grpc/test/bufconn"
1819
"google.golang.org/protobuf/encoding/protojson"
1920
protoref "google.golang.org/protobuf/reflect/protoreflect"
@@ -5445,3 +5446,69 @@ func Test_Datasource_Load_WithHeaders(t *testing.T) {
54455446
})
54465447
}
54475448
}
5449+
5450+
func Test_Datasource_Load_PreservesExistingContextMetadata(t *testing.T) {
5451+
conn, cleanup := setupTestGRPCServer(t)
5452+
t.Cleanup(cleanup)
5453+
5454+
type graphqlError struct {
5455+
Message string `json:"message"`
5456+
}
5457+
type graphqlResponse struct {
5458+
Data map[string]interface{} `json:"data"`
5459+
Errors []graphqlError `json:"errors,omitempty"`
5460+
}
5461+
5462+
// Parse the GraphQL schema
5463+
schemaDoc := grpctest.MustGraphQLSchema(t)
5464+
5465+
query := `query UserQuery($id: ID!) { user(id: $id) { id name } }`
5466+
vars := `{"variables":{"id":"test-user-123"}}`
5467+
5468+
// Parse the GraphQL query
5469+
queryDoc, report := astparser.ParseGraphqlDocumentString(query)
5470+
require.False(t, report.HasErrors(), "failed to parse query: %s", report.Error())
5471+
5472+
compiler, err := NewProtoCompiler(grpctest.MustProtoSchema(t), testMapping())
5473+
require.NoError(t, err)
5474+
5475+
// Create the datasource
5476+
ds, err := NewDataSource(conn, DataSourceConfig{
5477+
Operation: &queryDoc,
5478+
Definition: &schemaDoc,
5479+
SubgraphName: "Products",
5480+
Mapping: testMapping(),
5481+
Compiler: compiler,
5482+
})
5483+
require.NoError(t, err)
5484+
5485+
// Create a context with existing metadata
5486+
ctx := metadata.NewOutgoingContext(
5487+
context.Background(),
5488+
metadata.Pairs("x-existing-key", "existing-value"),
5489+
)
5490+
5491+
// Create HTTP headers to be forwarded
5492+
headers := make(http.Header)
5493+
headers.Set("X-User-ID", "header-user-456")
5494+
5495+
// Execute the query with both existing context metadata and new HTTP headers
5496+
input := fmt.Sprintf(`{"query":%q,"body":%s}`, query, vars)
5497+
output, err := ds.Load(ctx, headers, []byte(input))
5498+
require.NoError(t, err)
5499+
5500+
// Parse the response
5501+
var resp graphqlResponse
5502+
err = json.Unmarshal(output, &resp)
5503+
require.NoError(t, err, "Failed to unmarshal response")
5504+
5505+
// Verify no errors
5506+
require.Empty(t, resp.Errors, "Should not have GraphQL errors")
5507+
5508+
// Verify the response includes both the header-derived ID and the existing metadata value
5509+
user, ok := resp.Data["user"].(map[string]interface{})
5510+
require.True(t, ok, "user should be an object")
5511+
require.Equal(t, "header-user-456", user["id"], "user ID should come from HTTP header")
5512+
require.Equal(t, "User header-user-456 (existing: existing-value)", user["name"],
5513+
"user name should include both header-derived ID and existing context metadata")
5514+
}

v2/pkg/grpctest/mockservice.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,22 +236,33 @@ func (s *MockService) QueryUsers(ctx context.Context, in *productv1.QueryUsersRe
236236

237237
func (s *MockService) QueryUser(ctx context.Context, in *productv1.QueryUserRequest) (*productv1.QueryUserResponse, error) {
238238
userId := in.GetId()
239+
existingValue := ""
239240
md, ok := metadata.FromIncomingContext(ctx)
240241
if ok {
241242
if values := md.Get("x-user-id"); len(values) > 0 {
242243
userId = values[0]
243244
}
245+
// Check for existing metadata that was in the context before headers were added
246+
if values := md.Get("x-existing-key"); len(values) > 0 {
247+
existingValue = values[0]
248+
}
244249
}
245250

246251
// Return a gRPC status error for a specific test case
247252
if userId == "error-user" {
248253
return nil, status.Errorf(codes.NotFound, "user not found: %s", userId)
249254
}
250255

256+
// Include existing metadata value in the name if present
257+
userName := fmt.Sprintf("User %s", userId)
258+
if existingValue != "" {
259+
userName = fmt.Sprintf("User %s (existing: %s)", userId, existingValue)
260+
}
261+
251262
return &productv1.QueryUserResponse{
252263
User: &productv1.User{
253264
Id: userId,
254-
Name: fmt.Sprintf("User %s", userId),
265+
Name: userName,
255266
},
256267
}, nil
257268
}

0 commit comments

Comments
 (0)