diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index c2f998eae..b7c610968 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -127,8 +127,8 @@ func NewTracer(tracer trace.Tracer, capturedRequestHeaders, capturedResponseHead return &Tracer{ Tracer: tracer, safeQueryParams: safeQueryParams, - capturedRequestHeaders: capturedRequestHeaders, - capturedResponseHeaders: capturedResponseHeaders, + capturedRequestHeaders: canonicalizeHeaders(capturedRequestHeaders), + capturedResponseHeaders: canonicalizeHeaders(capturedResponseHeaders), } } @@ -346,3 +346,18 @@ func defaultStatus(code int) (codes.Code, string) { } return codes.Unset, "" } + +// canonicalizeHeaders converts a slice of header keys to their canonical form. +// It uses http.CanonicalHeaderKey to ensure that the headers are in a consistent format. +func canonicalizeHeaders(headers []string) []string { + if headers == nil { + return nil + } + + canonicalHeaders := make([]string, len(headers)) + for i, header := range headers { + canonicalHeaders[i] = http.CanonicalHeaderKey(header) + } + + return canonicalHeaders +} diff --git a/pkg/tracing/tracing_test.go b/pkg/tracing/tracing_test.go index e3cc39cea..e857703d8 100644 --- a/pkg/tracing/tracing_test.go +++ b/pkg/tracing/tracing_test.go @@ -17,6 +17,7 @@ import ( "github.com/traefik/traefik/v3/pkg/types" "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" ) func Test_safeFullURL(t *testing.T) { @@ -414,3 +415,52 @@ func TestTracerProvider(t *testing.T) { span.TracerProvider().Tracer("github.com/traefik/traefik") span.TracerProvider().Tracer("other") } + +// TestNewTracer_HeadersCanonicalization tests that NewTracer properly canonicalizes headers. +func TestNewTracer_HeadersCanonicalization(t *testing.T) { + testCases := []struct { + desc string + inputHeaders []string + expectedCanonicalHeaders []string + }{ + { + desc: "Empty headers", + inputHeaders: []string{}, + expectedCanonicalHeaders: []string{}, + }, + { + desc: "Already canonical headers", + inputHeaders: []string{"Content-Type", "User-Agent", "Accept-Encoding"}, + expectedCanonicalHeaders: []string{"Content-Type", "User-Agent", "Accept-Encoding"}, + }, + { + desc: "Lowercase headers", + inputHeaders: []string{"content-type", "user-agent", "accept-encoding"}, + expectedCanonicalHeaders: []string{"Content-Type", "User-Agent", "Accept-Encoding"}, + }, + { + desc: "Mixed case headers", + inputHeaders: []string{"CoNtEnT-tYpE", "uSeR-aGeNt", "aCcEpT-eNcOdInG"}, + expectedCanonicalHeaders: []string{"Content-Type", "User-Agent", "Accept-Encoding"}, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + // Create a mock tracer using a no-op tracer from OpenTelemetry + mockTracer := noop.NewTracerProvider().Tracer("test") + + // Test capturedRequestHeaders + tracer := NewTracer(mockTracer, test.inputHeaders, nil, nil) + assert.Equal(t, test.expectedCanonicalHeaders, tracer.capturedRequestHeaders) + assert.Nil(t, tracer.capturedResponseHeaders) + + // Test capturedResponseHeaders + tracer = NewTracer(mockTracer, nil, test.inputHeaders, nil) + assert.Equal(t, test.expectedCanonicalHeaders, tracer.capturedResponseHeaders) + assert.Nil(t, tracer.capturedRequestHeaders) + }) + } +}