Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions pkg/grpc/requestinfo/requestinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,27 @@ func Test_gRPCGateway(t *testing.T) {
assert.Equal(t, "test agent", receivedRI.HTTPRequest.Headers.Get(userAgentKey))
assert.Equal(t, "test value", receivedRI.HTTPRequest.Headers.Get("test-key"))

// Verify that metadata contains both the gateway's own User-Agent
// (from grpc.WithUserAgent) and the original HTTP User-Agent
// (forwarded by the grpc-gateway under a prefixed key).
assert.NotNil(t, receivedRI.Metadata)
assert.Equal(t, []string{"application/grpc"}, receivedRI.Metadata.Get("content-type"))
assert.Contains(t, receivedRI.Metadata.Get(userAgentKey)[0], "gateway agent")

// The gRPC transport User-Agent (gateway + grpc-go version).
mdUserAgents := receivedRI.Metadata.Get(userAgentKey)
require.Len(t, mdUserAgents, 1)
assert.Contains(t, mdUserAgents[0], "gateway agent")

// The original HTTP User-Agent is forwarded under the grpc-gateway
// prefixed key, not under "user-agent" directly.
prefixedUserAgentKey, _ := runtime.DefaultHeaderMatcher(userAgentKey)
assert.Contains(t, receivedRI.Metadata.Get(prefixedUserAgentKey)[0], "test agent")
gwUserAgents := receivedRI.Metadata.Get(prefixedUserAgentKey)
require.Len(t, gwUserAgents, 1)
assert.Equal(t, "test agent", gwUserAgents[0])

// The original HTTP User-Agent is also preserved in the embedded
// HTTP request (via AnnotateMD/RequestInfo).
assert.Equal(t, "test agent", receivedRI.HTTPRequest.Headers.Get(userAgentKey))
}

func Test_Conversions(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/telemetry/phonehome/headers_multimap.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ func (h Headers) GetMatching(key string, value glob.Pattern) []string {
return result
}

// Set implements the setter interface.
// Set overrides the current value(s) or deletes the key if no values provided.
func (h Headers) Set(key string, values ...string) {
http.Header(h).Del(key)
for i, value := range values {
if i == 0 {
http.Header(h).Set(key, value)
Expand Down
15 changes: 5 additions & 10 deletions pkg/telemetry/phonehome/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,18 @@ func getGRPCRequestDetails(ctx context.Context, err error, grpcFullMethod string
if ri.HTTPRequest.URL != nil {
path = ri.HTTPRequest.URL.Path
}
// Override the User-Agent with the gRPC client or the grpc-gateway user
// agent.
grpcClientAgent := ri.Metadata.Get(userAgentHeaderKey)
if clientAgent := ri.HTTPRequest.Headers.Get(userAgentHeaderKey); clientAgent != "" {
grpcClientAgent = append(grpcClientAgent, clientAgent)
// Append the gRPC transport User-Agent from metadata to the
// original HTTP headers so all User-Agent values are under one key.
for _, ua := range ri.Metadata.Get(userAgentHeaderKey) {
ri.HTTPRequest.Headers.Add(userAgentHeaderKey, ua)
}
header := Headers(ri.HTTPRequest.Headers)
// The request has already been processed (we've got the result), so the
// headers are ok to modify to avoid cloning.
header.Set(userAgentHeaderKey, grpcClientAgent...)
return &RequestParams{
UserID: id,
Method: ri.HTTPRequest.Method,
Path: path,
Code: grpcError.ErrToHTTPStatus(err),
GRPCReq: req,
Headers: header,
Headers: Headers(ri.HTTPRequest.Headers),
}
}

Expand Down
50 changes: 49 additions & 1 deletion pkg/telemetry/phonehome/interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,68 @@ func (s *interceptorTestSuite) TestGrpcWithHTTPRequestInfo() {
rih := requestinfo.NewRequestInfoHandler()
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: &net.UnixAddr{Net: "pipe"}})
md := rih.AnnotateMD(ctx, req)
// Simulate the gRPC transport User-Agent (set via grpc.WithUserAgent).
md.Set(userAgentHeaderKey, "gateway")

ctx, err := rih.UpdateContextForGRPC(metadata.NewIncomingContext(ctx, md))
s.NoError(err)

rp := getGRPCRequestDetails(ctx, err, "ignored grpc method", "request")
s.Equal(http.StatusOK, rp.Code)
s.Equal([]string{"gateway", "user"}, rp.Headers.Get(userAgentHeaderKey))
// Original HTTP User-Agent + gRPC transport agent merged under one key.
s.Equal([]string{"user", "gateway"}, rp.Headers.Get(userAgentHeaderKey))
s.Nil(rp.UserID)
s.Equal("request", rp.GRPCReq)
s.Equal("/wrapped/http", rp.Path)
s.Equal(http.MethodPatch, rp.Method)
}

func (s *interceptorTestSuite) TestGrpcWithHTTPRequestInfo_UserAgentVariants() {
cases := map[string]struct {
httpUserAgent []string // User-Agent values on the HTTP request.
mdUserAgent []string // User-Agent values in gRPC metadata (transport agent).
expected []string // Expected merged User-Agent values in the result.
}{
"HTTP and gRPC transport User-Agent": {
httpUserAgent: []string{"curl/8.0"},
mdUserAgent: []string{"Rox Central/4.11 grpc-go/1.80.0"},
expected: []string{"curl/8.0", "Rox Central/4.11 grpc-go/1.80.0"},
},
"only HTTP User-Agent": {
httpUserAgent: []string{"curl/8.0"},
expected: []string{"curl/8.0"},
},
"only gRPC transport User-Agent": {
mdUserAgent: []string{"grpc-go/1.80.0"},
expected: []string{"grpc-go/1.80.0"},
},
"no User-Agent anywhere": {
expected: nil,
},
}
for name, tc := range cases {
s.Run(name, func() {
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
req.Header.Del(userAgentHeaderKey)
for _, ua := range tc.httpUserAgent {
req.Header.Add(userAgentHeaderKey, ua)
}
rih := requestinfo.NewRequestInfoHandler()
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: &net.UnixAddr{Net: "pipe"}})
md := rih.AnnotateMD(ctx, req)
if tc.mdUserAgent != nil {
md.Set(userAgentHeaderKey, tc.mdUserAgent...)
}

ctx, err := rih.UpdateContextForGRPC(metadata.NewIncomingContext(ctx, md))
s.NoError(err)

rp := getGRPCRequestDetails(ctx, err, "ignored", "request")
s.Equal(tc.expected, rp.Headers.Get(userAgentHeaderKey))
})
}
}

type testBody struct {
N int `json:"n"`
}
Expand Down
Loading