Skip to content

Commit 1fc2cfb

Browse files
kevwanCopilot
andauthored
fix: gateway trace headers 5248 (#5256)
Co-authored-by: Copilot <[email protected]>
1 parent 942cdae commit 1fc2cfb

File tree

2 files changed

+114
-2
lines changed

2 files changed

+114
-2
lines changed

gateway/internal/headerprocessor.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,40 @@ const (
1111
metadataPrefix = "gateway-"
1212
)
1313

14+
// OpenTelemetry trace propagation headers that need to be forwarded to gRPC metadata.
15+
// These headers are used by the W3C Trace Context standard for distributed tracing.
16+
var traceHeaders = map[string]bool{
17+
"traceparent": true,
18+
"tracestate": true,
19+
"baggage": true,
20+
}
21+
1422
// ProcessHeaders builds the headers for the gateway from HTTP headers.
23+
// It forwards both custom metadata headers (with Grpc-Metadata- prefix)
24+
// and OpenTelemetry trace propagation headers (traceparent, tracestate, baggage)
25+
// to ensure distributed tracing works correctly across the gateway.
1526
func ProcessHeaders(header http.Header) []string {
1627
var headers []string
1728

1829
for k, v := range header {
30+
// Forward OpenTelemetry trace propagation headers
31+
// These must be lowercase per gRPC metadata conventions
32+
if lowerKey := strings.ToLower(k); traceHeaders[lowerKey] {
33+
for _, vv := range v {
34+
headers = append(headers, lowerKey+":"+vv)
35+
}
36+
continue
37+
}
38+
39+
// Forward custom metadata headers with Grpc-Metadata- prefix
1940
if !strings.HasPrefix(k, metadataHeaderPrefix) {
2041
continue
2142
}
2243

23-
key := fmt.Sprintf("%s%s", metadataPrefix, strings.TrimPrefix(k, metadataHeaderPrefix))
44+
// gRPC metadata keys are case-insensitive and stored as lowercase,
45+
// so we lowercase the key to match gRPC conventions
46+
trimmedKey := strings.TrimPrefix(k, metadataHeaderPrefix)
47+
key := strings.ToLower(fmt.Sprintf("%s%s", metadataPrefix, trimmedKey))
2448
for _, vv := range v {
2549
headers = append(headers, key+":"+vv)
2650
}

gateway/internal/headerprocessor_test.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,93 @@ func TestBuildHeadersWithValues(t *testing.T) {
1818
req := httptest.NewRequest("GET", "/", http.NoBody)
1919
req.Header.Add("grpc-metadata-a", "b")
2020
req.Header.Add("grpc-metadata-b", "b")
21-
assert.ElementsMatch(t, []string{"gateway-A:b", "gateway-B:b"}, ProcessHeaders(req.Header))
21+
assert.ElementsMatch(t, []string{"gateway-a:b", "gateway-b:b"}, ProcessHeaders(req.Header))
22+
}
23+
24+
func TestProcessHeadersWithTraceContext(t *testing.T) {
25+
req := httptest.NewRequest("GET", "/", http.NoBody)
26+
req.Header.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
27+
req.Header.Set("tracestate", "key1=value1,key2=value2")
28+
req.Header.Set("baggage", "userId=alice,serverNode=DF:28")
29+
30+
headers := ProcessHeaders(req.Header)
31+
32+
assert.Len(t, headers, 3)
33+
assert.Contains(t, headers, "traceparent:00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
34+
assert.Contains(t, headers, "tracestate:key1=value1,key2=value2")
35+
assert.Contains(t, headers, "baggage:userId=alice,serverNode=DF:28")
36+
}
37+
38+
func TestProcessHeadersWithMixedHeaders(t *testing.T) {
39+
req := httptest.NewRequest("GET", "/", http.NoBody)
40+
req.Header.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
41+
req.Header.Set("grpc-metadata-custom", "value1")
42+
req.Header.Set("content-type", "application/json")
43+
req.Header.Set("tracestate", "key1=value1")
44+
45+
headers := ProcessHeaders(req.Header)
46+
47+
// Should include trace headers and grpc-metadata headers, but not regular headers
48+
assert.Len(t, headers, 3)
49+
assert.Contains(t, headers, "traceparent:00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
50+
assert.Contains(t, headers, "tracestate:key1=value1")
51+
assert.Contains(t, headers, "gateway-custom:value1")
52+
}
53+
54+
func TestProcessHeadersTraceparentCaseInsensitive(t *testing.T) {
55+
tests := []struct {
56+
name string
57+
headerKey string
58+
headerVal string
59+
expectedKey string
60+
}{
61+
{
62+
name: "lowercase traceparent",
63+
headerKey: "traceparent",
64+
headerVal: "00-trace-span-01",
65+
expectedKey: "traceparent",
66+
},
67+
{
68+
name: "uppercase Traceparent",
69+
headerKey: "Traceparent",
70+
headerVal: "00-trace-span-01",
71+
expectedKey: "traceparent",
72+
},
73+
{
74+
name: "mixed case TraceParent",
75+
headerKey: "TraceParent",
76+
headerVal: "00-trace-span-01",
77+
expectedKey: "traceparent",
78+
},
79+
{
80+
name: "lowercase tracestate",
81+
headerKey: "tracestate",
82+
headerVal: "key=value",
83+
expectedKey: "tracestate",
84+
},
85+
{
86+
name: "mixed case TraceState",
87+
headerKey: "TraceState",
88+
headerVal: "key=value",
89+
expectedKey: "tracestate",
90+
},
91+
}
92+
93+
for _, tt := range tests {
94+
t.Run(tt.name, func(t *testing.T) {
95+
req := httptest.NewRequest("GET", "/", http.NoBody)
96+
req.Header.Set(tt.headerKey, tt.headerVal)
97+
98+
headers := ProcessHeaders(req.Header)
99+
100+
assert.Len(t, headers, 1)
101+
assert.Contains(t, headers, tt.expectedKey+":"+tt.headerVal)
102+
})
103+
}
104+
}
105+
106+
func TestProcessHeadersEmptyHeaders(t *testing.T) {
107+
req := httptest.NewRequest("GET", "/", http.NoBody)
108+
headers := ProcessHeaders(req.Header)
109+
assert.Empty(t, headers)
22110
}

0 commit comments

Comments
 (0)