From 91029d33ea5e5f130f7f20ee166b4e07a817832a Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 8 Apr 2026 16:22:22 -0700 Subject: [PATCH 01/52] add tests that check spec compatibility --- .../Propagation/BaggagePropagatorTests.cs | 489 +++++++++++++++++- 1 file changed, 482 insertions(+), 7 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index b6633bdb53b..6e1e095dab5 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -154,13 +154,13 @@ public void ValidateSpecialCharsBaggageExtraction() Assert.Equal(3, actualBaggage.Count); - Assert.True(actualBaggage.ContainsKey("key 1")); - Assert.Equal("value 1", actualBaggage["key 1"]); + Assert.True(actualBaggage.ContainsKey("key+1")); + Assert.Equal("value+1", actualBaggage["key+1"]); Assert.True(actualBaggage.ContainsKey("key2")); Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]); - Assert.True(actualBaggage.ContainsKey("key()3")); + Assert.True(!actualBaggage.ContainsKey("key()3")); Assert.Equal("value()!&;:", actualBaggage["key()3"]); } @@ -238,7 +238,7 @@ public void ValidateEmptyValueSkipped() Assert.Empty(propagationContext.Baggage.GetBaggage()); } - [Fact(Skip = "Fails due to spec mismatch, tracked in https://github.com/open-telemetry/opentelemetry-dotnet/issues/5210")] + [Fact] public void ValidateOWSOnExtraction() { var carrier = new Dictionary @@ -259,7 +259,7 @@ public void ValidateOWSOnExtraction() Assert.Equal("SomeValue2", baggage[1].Value); } - [Fact(Skip = "Fails due to spec mismatch, tracked in https://github.com/open-telemetry/opentelemetry-dotnet/issues/5210")] + [Fact] public void ValidateSemicolonMetadataIgnoredOnExtraction() { var carrier = new Dictionary @@ -276,6 +276,39 @@ public void ValidateSemicolonMetadataIgnoredOnExtraction() Assert.Equal("SomeValue", baggage.Value); } + [Fact] + public void ValidateOptionalWhiteSpaceExtractionDoesNotCorruptOnReinjection() + { + // Simulates a header emitted by .NET 10's W3C propagator + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "correlationId = 12345, userId = user-abc" }, + }; + + var extractedContext = this.baggage.Extract(default, carrier, Getter); + + var outboundCarrier = new Dictionary(); + this.baggage.Inject(extractedContext, outboundCarrier, Setter); + + Assert.Equal("correlationId=12345,userId=user-abc", outboundCarrier[BaggagePropagator.BaggageHeaderName]); + } + + [Fact] + public void ValidateOptionalWhiteSpaceBeforeSemicolonIgnored() + { + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "SomeKey=SomeValue ; propertyKey=propertyValue" }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + + var baggage = Assert.Single(propagationContext.Baggage.GetBaggage()); + + Assert.Equal("SomeKey", baggage.Key); + Assert.Equal("SomeValue", baggage.Value); + } + [Fact] public void ValidatePercentEncoding() { @@ -446,8 +479,8 @@ public void ValidateValueWithMultipleEqualsPreservesEquals() Assert.Equal("value=more=equals", extractedBaggage["key"]); } - [Fact(Skip = "Fails due to spec mismatch, tracked in https://github.com/open-telemetry/opentelemetry-dotnet/issues/5210")] - public void ValidateSpecialCharactersInjection() + [Fact] + public void ValidateSpecialCharactersInjectionForValue() { var propagationContext = new PropagationContext( default, @@ -471,4 +504,446 @@ public void ValidateSpecialCharactersInjection() Assert.Equal("\t \"';=asdf!@#$%^&*()", extractedBaggage["key"]); } + + [Fact] + public void KeyValidTcharSymbolInjectedUnchanged() + { + // Keys composed entirely of tchar characters must appear unchanged in + // the injected header. Keys are never percent-encoded. + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary + { + { "my.key-name_here", "value" }, + })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Equal("my.key-name_here=value", carrier[BaggagePropagator.BaggageHeaderName]); + } + + // ------------------------------------------------------------------------- + // Keys — incorrect path + // The current implementation URL-decodes keys on extract (#5479). + // These tests document what the correct behaviour SHOULD be: keys arriving + // on the wire that happen to contain %-sequences or '+' must be treated as + // literal token strings — they are NOT decoded. + // + // '%' is a valid tchar, so "key%20name" is a valid token whose name is + // literally "key%20name", not "key name". '+' is also a valid tchar. + // ------------------------------------------------------------------------- + [Fact] + public void KeyPercentSequenceInKeyPreservedLiterallyOnExtract() + { + // '%' is a valid tchar. "key%20name" is a valid token whose literal + // name is "key%20name". The extractor must NOT decode it to "key name". + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "key%20name=value,valid-key=valid-value" }, + }; + + var context = this.baggage.Extract(default, carrier, Getter); + var baggage = context.Baggage.GetBaggage(); + + Assert.Equal(2, baggage.Count); + Assert.True(baggage.ContainsKey("key%20name")); // literal token, not decoded + Assert.False(baggage.ContainsKey("key name")); // must NOT have been decoded + } + + [Fact] + public void KeyPlusInKeyPreservedLiterallyOnExtract() + { + // '+' is a valid tchar. "key+name" is a valid token whose literal name + // is "key+name". The extractor must NOT decode '+' to a space. + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "key+name=value" }, + }; + + var context = this.baggage.Extract(default, carrier, Getter); + var entry = Assert.Single(context.Baggage.GetBaggage()); + + Assert.Equal("key+name", entry.Key); // '+' is literal, not a space + } + + [Theory] + [InlineData(" ")] + [InlineData("\"")] + [InlineData("(")] + [InlineData(")")] + [InlineData(",")] + [InlineData("/")] + [InlineData(":")] + [InlineData(";")] + [InlineData("<")] + [InlineData("=")] + [InlineData(">")] + [InlineData("?")] + [InlineData("@")] + [InlineData("[")] + [InlineData("\\")] + [InlineData("]")] + [InlineData("{")] + [InlineData("}")] + public void KeyWithDelimiterCharEntirePairDroppedOnExtract(string delimiter) + { + // A key containing any delimiter character is an invalid token. + // The pair containing it must be silently dropped. The remaining + // valid pair must still be extracted. + var invalidKey = $"key{delimiter}name"; + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, $"{invalidKey}=should-drop,valid-key=valid-value" }, + }; + + var context = this.baggage.Extract(default, carrier, Getter); + var entry = Assert.Single(context.Baggage.GetBaggage()); + + Assert.Equal("valid-key", entry.Key); + Assert.Equal("valid-value", entry.Value); + } + + [Theory] + [InlineData(" ")] + [InlineData("(")] + [InlineData(":")] + [InlineData(";")] + [InlineData("@")] + public void KeyWithDelimiterCharEntirePairDroppedOnInject(string delimiter) + { + // A key containing a delimiter is an invalid token. On inject the + // pair must be dropped — not percent-encoded, not partially written. + // Other valid pairs in the same baggage must still be injected. + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary + { + { $"invalid{delimiter}key", "should-be-dropped" }, + { "valid-key", "valid-value" }, + })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Single(carrier); + Assert.Equal("valid-key=valid-value", carrier[BaggagePropagator.BaggageHeaderName]); + } + + [Theory] + [InlineData("!")] + [InlineData("#")] + [InlineData("&")] + [InlineData("*")] + [InlineData("+")] + [InlineData("-")] + [InlineData("/")] + [InlineData(":")] + [InlineData("=")] + [InlineData("?")] + [InlineData("@")] + [InlineData("~")] + public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) + { + // These characters are valid unencoded baggage-octets and must not be + // transformed on either inject or extract. + var value = $"val{octet}ue"; + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", value } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + // Confirm it was injected without encoding + Assert.Contains(value, carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + + // Confirm it round-trips back unchanged + var extracted = this.baggage.Extract(default, carrier, Getter); + Assert.Equal(value, extracted.Baggage.GetBaggage()["key"]); + } + + [Theory] + [InlineData("v=1")] + [InlineData("v=1=2")] + [InlineData("a=b=c=d")] + public void ValueWithEqualsSignsExtractedCorrectly(string value) + { + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, $"key={value}" }, + }; + + var context = this.baggage.Extract(default, carrier, Getter); + var entry = Assert.Single(context.Baggage.GetBaggage()); + + Assert.Equal("key", entry.Key); + Assert.Equal(value, entry.Value); + } + + [Theory] + [InlineData("key%201=value%201", "key%201", "value 1")] + [InlineData("key=val+ue", "key", "val+ue")] + [InlineData("key=val%2Bue", "key", "val+ue")] + [InlineData("key=val%20ue", "key", "val ue")] + [InlineData("key=value=1", "key", "value=1")] + [InlineData("key=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~", "key", " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] + public void ValidateMiscTests(string propagatedBaggage, string key, string expectedDecodedValue) + { + var carrier = new List> + { + new(BaggagePropagator.BaggageHeaderName, propagatedBaggage), + }; + + var propagationContext = this.baggage.Extract(default, carrier, GetterList); + + Assert.False(propagationContext == default); + Assert.True(propagationContext.ActivityContext == default); + + Assert.Equal(1, propagationContext.Baggage.Count); + + var actualBaggage = propagationContext.Baggage.GetBaggage(); + + Assert.Single(actualBaggage); + + Assert.Contains(key, actualBaggage); + Assert.Equal(expectedDecodedValue, actualBaggage[key]); + } + + [Fact] + public void ValidatePlusInValueIsLiteralOnExtract() + { + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "key=value+1" }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Single(propagationContext.Baggage.GetBaggage()); + + var entry = propagationContext.Baggage.GetBaggage().First(); + Assert.Equal("key", entry.Key); + Assert.Equal("value+1", entry.Value); // '+' stays as '+', not ' ' + } + + [Theory] + [InlineData("!")] + [InlineData("#")] + [InlineData("$")] + [InlineData("%")] + [InlineData("&")] + [InlineData("'")] + [InlineData("*")] + [InlineData("+")] + [InlineData("-")] + [InlineData(".")] + [InlineData("^")] + [InlineData("_")] + [InlineData("`")] + [InlineData("|")] + [InlineData("~")] + public void ValidateValidTcharInKeyIsAcceptedOnExtract(string specialChar) + { + // This test is for all tchar characters are valid. + var key = $"key{specialChar}name"; + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, $"prefix{key}suffix=value" }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Single(propagationContext.Baggage.GetBaggage()); + Assert.Equal(key, propagationContext.Baggage.GetBaggage().First().Key); + } + + [Theory] + [InlineData(" ")] + [InlineData("\"")] + [InlineData("(")] + [InlineData(")")] + [InlineData(",")] + [InlineData("/")] + [InlineData(":")] + [InlineData(";")] + [InlineData("<")] + [InlineData("=")] + [InlineData(">")] + [InlineData("?")] + [InlineData("@")] + [InlineData("[")] + [InlineData("\\")] + [InlineData("]")] + [InlineData("{")] + [InlineData("}")] + public void ValidateKeyWithInvalidTcharDroppedOnExtract(string invalidChar) + { + var invalidKey = $"key{invalidChar}name"; + var carrier = new Dictionary + { + { + BaggagePropagator.BaggageHeaderName, + $"{invalidKey}=should-be-dropped,valid-key=valid-value" + }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Single(propagationContext.Baggage.GetBaggage()); + Assert.Equal("valid-key", propagationContext.Baggage.GetBaggage().First().Key); + } + + [Theory] + [InlineData("%21", "!")] + [InlineData("%22", "\"")] + [InlineData("%23", "#")] + [InlineData("%24", "$")] + [InlineData("%25", "%")] + [InlineData("%26", "&")] + [InlineData("%27", "'")] + [InlineData("%28", "(")] + [InlineData("%29", ")")] + [InlineData("%2A", "*")] + [InlineData("%2B", "+")] + [InlineData("%2C", ",")] + [InlineData("%2D", "-")] + [InlineData("%2E", ".")] + [InlineData("%2F", "/")] + [InlineData("%3A", ":")] + [InlineData("%3B", ";")] + [InlineData("%3C", "<")] + [InlineData("%3D", "=")] + [InlineData("%3E", ">")] + [InlineData("%3F", "?")] + [InlineData("%40", "@")] + [InlineData("%5B", "[")] + [InlineData("%5C", "\\")] + [InlineData("%5D", "]")] + [InlineData("%5E", "^")] + [InlineData("%5F", "_")] + [InlineData("%60", "`")] + [InlineData("%7B", "{")] + [InlineData("%7C", "|")] + [InlineData("%7D", "}")] + [InlineData("%7E", "~")] + [InlineData("%20", " ")] + public void ValidatePercentEncodedValueCharacterDecodesCorrectly(string encoded, string expected) + { + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, $"key=prefix{encoded}suffix" }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Single(propagationContext.Baggage.GetBaggage()); + Assert.Equal(expected, propagationContext.Baggage.GetBaggage().First().Value); + } + + [Theory] + [InlineData("key=%")] + [InlineData("key=%2")] + [InlineData("key=%GG")] + public void ValidateMalformedPercentSequenceInValueIsHandledGracefully(string headerValue) + { + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, headerValue }, + }; + + var exception = Record.Exception(() => + this.baggage.Extract(default, carrier, Getter)); + + Assert.Null(exception); + } + + [Theory] + [InlineData(" ", "%20")] + [InlineData("\"", "%22")] + [InlineData(",", "%2C")] + [InlineData(";", "%3B")] + [InlineData("\\", "%5C")] + public void ValidateCharacterOutsideBaggageOctetIsPercentEncodedOnInject( + string rawChar, string expectedEncoded) + { + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", $"val{rawChar}ue" } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Single(carrier); + Assert.Contains(expectedEncoded, carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + } + + // ========================================================================= + // ROUND-TRIP + // These tests inject baggage and then extract from the same carrier, + // verifying that the full pipeline preserves the original data. + // ========================================================================= + + [Fact] + public void RoundTripValueWithMultipleEqualsPreservedExactly() + { + var carrier = new Dictionary(); + this.baggage.Inject( + new PropagationContext(default, new Baggage(new Dictionary + { + { "key", "value=more=equals" }, + })), + carrier, Setter); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + Assert.Equal("value=more=equals", extracted["key"]); + } + + [Fact] + public void RoundTripValueWithSpacePreservedAsSpace() + { + + var carrier = new Dictionary(); + this.baggage.Inject( + new PropagationContext(default, new Baggage(new Dictionary { + { "key", "value with space" }, + })), carrier, Setter); + + // The intermediate header must use %20, not '+' + Assert.Contains("%20", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + Assert.DoesNotContain("+", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + Assert.Equal("value with space", extracted["key"]); + } + + [Fact] + public void RoundTripValueWithAllMandatoryEncodeCharsPreservedExactly() + { + const string original = "val ue\"wi,th;back\\slash"; + + var carrier = new Dictionary(); + this.baggage.Inject( + new PropagationContext(default, new Baggage(new Dictionary + { + { "key", original }, + })), + carrier, Setter); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + Assert.Equal(original, extracted["key"]); + } + + [Fact] + public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() + { + var carrier = new Dictionary(); + this.baggage.Inject( + new PropagationContext(default, new Baggage(new Dictionary + { + { "valid-key", "valid-value" }, + { "invalid key", "should-be-dropped" }, // space is not tchar + })), + carrier, Setter); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + Assert.Single(extracted); + Assert.Equal("valid-value", extracted["valid-key"]); + } } From 10ae63c1e2679c0ad4f78aa43f066b7903b30949 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 8 Apr 2026 16:43:34 -0700 Subject: [PATCH 02/52] linter and comments fixed --- .../Propagation/BaggagePropagatorTests.cs | 49 ++++--------------- 1 file changed, 10 insertions(+), 39 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 6e1e095dab5..e5c997e8b02 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -495,9 +495,9 @@ public void ValidateSpecialCharactersInjectionForValue() var baggageHeader = carrier[BaggagePropagator.BaggageHeaderName]; - Assert.Contains("%09", baggageHeader, StringComparison.Ordinal); // Tab - Assert.Contains("%20", baggageHeader, StringComparison.Ordinal); // Space - Assert.Contains("%22", baggageHeader, StringComparison.Ordinal); // Quote + Assert.Contains("%09", baggageHeader, StringComparison.Ordinal); + Assert.Contains("%20", baggageHeader, StringComparison.Ordinal); + Assert.Contains("%22", baggageHeader, StringComparison.Ordinal); var extractedContext = this.baggage.Extract(default, carrier, Getter); var extractedBaggage = extractedContext.Baggage.GetBaggage(); @@ -508,8 +508,6 @@ public void ValidateSpecialCharactersInjectionForValue() [Fact] public void KeyValidTcharSymbolInjectedUnchanged() { - // Keys composed entirely of tchar characters must appear unchanged in - // the injected header. Keys are never percent-encoded. var propagationContext = new PropagationContext( default, new Baggage(new Dictionary @@ -536,8 +534,6 @@ public void KeyValidTcharSymbolInjectedUnchanged() [Fact] public void KeyPercentSequenceInKeyPreservedLiterallyOnExtract() { - // '%' is a valid tchar. "key%20name" is a valid token whose literal - // name is "key%20name". The extractor must NOT decode it to "key name". var carrier = new Dictionary { { BaggagePropagator.BaggageHeaderName, "key%20name=value,valid-key=valid-value" }, @@ -547,15 +543,13 @@ public void KeyPercentSequenceInKeyPreservedLiterallyOnExtract() var baggage = context.Baggage.GetBaggage(); Assert.Equal(2, baggage.Count); - Assert.True(baggage.ContainsKey("key%20name")); // literal token, not decoded - Assert.False(baggage.ContainsKey("key name")); // must NOT have been decoded + Assert.True(baggage.ContainsKey("key%20name")); + Assert.False(baggage.ContainsKey("key name")); } [Fact] public void KeyPlusInKeyPreservedLiterallyOnExtract() { - // '+' is a valid tchar. "key+name" is a valid token whose literal name - // is "key+name". The extractor must NOT decode '+' to a space. var carrier = new Dictionary { { BaggagePropagator.BaggageHeaderName, "key+name=value" }, @@ -564,7 +558,7 @@ public void KeyPlusInKeyPreservedLiterallyOnExtract() var context = this.baggage.Extract(default, carrier, Getter); var entry = Assert.Single(context.Baggage.GetBaggage()); - Assert.Equal("key+name", entry.Key); // '+' is literal, not a space + Assert.Equal("key+name", entry.Key); } [Theory] @@ -588,9 +582,6 @@ public void KeyPlusInKeyPreservedLiterallyOnExtract() [InlineData("}")] public void KeyWithDelimiterCharEntirePairDroppedOnExtract(string delimiter) { - // A key containing any delimiter character is an invalid token. - // The pair containing it must be silently dropped. The remaining - // valid pair must still be extracted. var invalidKey = $"key{delimiter}name"; var carrier = new Dictionary { @@ -612,9 +603,6 @@ public void KeyWithDelimiterCharEntirePairDroppedOnExtract(string delimiter) [InlineData("@")] public void KeyWithDelimiterCharEntirePairDroppedOnInject(string delimiter) { - // A key containing a delimiter is an invalid token. On inject the - // pair must be dropped — not percent-encoded, not partially written. - // Other valid pairs in the same baggage must still be injected. var propagationContext = new PropagationContext( default, new Baggage(new Dictionary @@ -885,11 +873,7 @@ public void RoundTripValueWithMultipleEqualsPreservedExactly() { var carrier = new Dictionary(); this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary - { - { "key", "value=more=equals" }, - })), - carrier, Setter); + new PropagationContext(default, new Baggage(new Dictionary { { "key", "value=more=equals" }, })), carrier, Setter); var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); Assert.Equal("value=more=equals", extracted["key"]); @@ -898,12 +882,9 @@ public void RoundTripValueWithMultipleEqualsPreservedExactly() [Fact] public void RoundTripValueWithSpacePreservedAsSpace() { - var carrier = new Dictionary(); this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary { - { "key", "value with space" }, - })), carrier, Setter); + new PropagationContext(default, new Baggage(new Dictionary { { "key", "value with space" }, })), carrier, Setter); // The intermediate header must use %20, not '+' Assert.Contains("%20", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); @@ -919,12 +900,7 @@ public void RoundTripValueWithAllMandatoryEncodeCharsPreservedExactly() const string original = "val ue\"wi,th;back\\slash"; var carrier = new Dictionary(); - this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary - { - { "key", original }, - })), - carrier, Setter); + this.baggage.Inject(new PropagationContext(default, new Baggage(new Dictionary { { "key", original }, })), carrier, Setter); var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); Assert.Equal(original, extracted["key"]); @@ -935,12 +911,7 @@ public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() { var carrier = new Dictionary(); this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary - { - { "valid-key", "valid-value" }, - { "invalid key", "should-be-dropped" }, // space is not tchar - })), - carrier, Setter); + new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, { "invalid key", "should-be-dropped" }, })), carrier, Setter); var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); Assert.Single(extracted); From c74ac9bc98f61bcfb4cd0164ed6e3e84f98a1c73 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 8 Apr 2026 16:45:31 -0700 Subject: [PATCH 03/52] lint --- .../Context/Propagation/BaggagePropagatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index e5c997e8b02..6f03841c990 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -522,11 +522,11 @@ public void KeyValidTcharSymbolInjectedUnchanged() } // ------------------------------------------------------------------------- - // Keys — incorrect path + // Keys incorrect path // The current implementation URL-decodes keys on extract (#5479). // These tests document what the correct behaviour SHOULD be: keys arriving // on the wire that happen to contain %-sequences or '+' must be treated as - // literal token strings — they are NOT decoded. + // literal token strings they are NOT decoded. // // '%' is a valid tchar, so "key%20name" is a valid token whose name is // literally "key%20name", not "key name". '+' is also a valid tchar. From 8d89a5ee6b6767f2fa8272488e82e22bf77d487d Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:10:47 -0700 Subject: [PATCH 04/52] key 3 should be dropped, no need to try and access the value --- .../Context/Propagation/BaggagePropagatorTests.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 6f03841c990..22a99366666 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -161,7 +161,6 @@ public void ValidateSpecialCharsBaggageExtraction() Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]); Assert.True(!actualBaggage.ContainsKey("key()3")); - Assert.Equal("value()!&;:", actualBaggage["key()3"]); } [Fact] @@ -868,17 +867,6 @@ public void ValidateCharacterOutsideBaggageOctetIsPercentEncodedOnInject( // verifying that the full pipeline preserves the original data. // ========================================================================= - [Fact] - public void RoundTripValueWithMultipleEqualsPreservedExactly() - { - var carrier = new Dictionary(); - this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary { { "key", "value=more=equals" }, })), carrier, Setter); - - var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); - Assert.Equal("value=more=equals", extracted["key"]); - } - [Fact] public void RoundTripValueWithSpacePreservedAsSpace() { From 2d5a39f6a8d97e53434504420974726af56b7dd8 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:12:49 -0700 Subject: [PATCH 05/52] - ' ' character (space) should be replaced by encoded value when injecting not '+' - replaced by tests: ValidateCharacterOutsideBaggageOctetIsPercentEncodedOnInject, RoundTripValueWithSpacePreservedAsSpace, ValidateSpecialCharactersInjectionForValue --- .../Propagation/BaggagePropagatorTests.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 22a99366666..a72e36cd6c0 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -190,24 +190,6 @@ public void ValidateBaggageInjection() Assert.Equal("key1=value1,key2=value2", carrier[BaggagePropagator.BaggageHeaderName]); } - [Fact] - public void ValidateSpecialCharsBaggageInjection() - { - var carrier = new Dictionary(); - var propagationContext = new PropagationContext( - default, - new Baggage(new Dictionary - { - { "key 1", "value 1" }, - { "key2", "!x_x,x-x&x(x\");:" }, - })); - - this.baggage.Inject(propagationContext, carrier, Setter); - - Assert.Single(carrier); - Assert.Equal("key+1=value+1,key2=!x_x%2Cx-x%26x(x%22)%3B%3A", carrier[BaggagePropagator.BaggageHeaderName]); - } - [Fact] public void ValidateMultipleEqualsInValue() { From 60b1906fe281507ac1b338c90379e6129e87906f Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:28:32 -0700 Subject: [PATCH 06/52] duplicate, already tested by ValidateValueWithMultipleEqualsPreservesEquals --- .../Propagation/BaggagePropagatorTests.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index a72e36cd6c0..32b860f43ff 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -632,24 +632,6 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) Assert.Equal(value, extracted.Baggage.GetBaggage()["key"]); } - [Theory] - [InlineData("v=1")] - [InlineData("v=1=2")] - [InlineData("a=b=c=d")] - public void ValueWithEqualsSignsExtractedCorrectly(string value) - { - var carrier = new Dictionary - { - { BaggagePropagator.BaggageHeaderName, $"key={value}" }, - }; - - var context = this.baggage.Extract(default, carrier, Getter); - var entry = Assert.Single(context.Baggage.GetBaggage()); - - Assert.Equal("key", entry.Key); - Assert.Equal(value, entry.Value); - } - [Theory] [InlineData("key%201=value%201", "key%201", "value 1")] [InlineData("key=val+ue", "key", "val+ue")] From 054d0053620d2178343562183e2b50f73cabfec8 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:29:47 -0700 Subject: [PATCH 07/52] duplicate to ValidateValueWithMultipleEqualsPreservesEquals --- .../Context/Propagation/BaggagePropagatorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 32b860f43ff..8225a3815f5 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -637,7 +637,6 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) [InlineData("key=val+ue", "key", "val+ue")] [InlineData("key=val%2Bue", "key", "val+ue")] [InlineData("key=val%20ue", "key", "val ue")] - [InlineData("key=value=1", "key", "value=1")] [InlineData("key=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~", "key", " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] public void ValidateMiscTests(string propagatedBaggage, string key, string expectedDecodedValue) { From e30a7b4616e993805ec66f412557fda0d9edd0bf Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:33:20 -0700 Subject: [PATCH 08/52] duplicate of ValidateKeyWithInvalidTcharDroppedOnExtract --- .../Propagation/BaggagePropagatorTests.cs | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 8225a3815f5..aedb1aea0f4 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -542,40 +542,6 @@ public void KeyPlusInKeyPreservedLiterallyOnExtract() Assert.Equal("key+name", entry.Key); } - [Theory] - [InlineData(" ")] - [InlineData("\"")] - [InlineData("(")] - [InlineData(")")] - [InlineData(",")] - [InlineData("/")] - [InlineData(":")] - [InlineData(";")] - [InlineData("<")] - [InlineData("=")] - [InlineData(">")] - [InlineData("?")] - [InlineData("@")] - [InlineData("[")] - [InlineData("\\")] - [InlineData("]")] - [InlineData("{")] - [InlineData("}")] - public void KeyWithDelimiterCharEntirePairDroppedOnExtract(string delimiter) - { - var invalidKey = $"key{delimiter}name"; - var carrier = new Dictionary - { - { BaggagePropagator.BaggageHeaderName, $"{invalidKey}=should-drop,valid-key=valid-value" }, - }; - - var context = this.baggage.Extract(default, carrier, Getter); - var entry = Assert.Single(context.Baggage.GetBaggage()); - - Assert.Equal("valid-key", entry.Key); - Assert.Equal("valid-value", entry.Value); - } - [Theory] [InlineData(" ")] [InlineData("(")] From 479f68600620f68863ddeecf4a206cacca5d8071 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:33:55 -0700 Subject: [PATCH 09/52] duplicate of ValidateMiscTests --- .../Context/Propagation/BaggagePropagatorTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index aedb1aea0f4..bdace7a83d6 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -600,7 +600,6 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) [Theory] [InlineData("key%201=value%201", "key%201", "value 1")] - [InlineData("key=val+ue", "key", "val+ue")] [InlineData("key=val%2Bue", "key", "val+ue")] [InlineData("key=val%20ue", "key", "val ue")] [InlineData("key=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~", "key", " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] From 858bff81e52cff382de20427401a1617d8655c07 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:35:26 -0700 Subject: [PATCH 10/52] duplicate of ValidatePercentEncodedComplexCharactersDecodesCorrectly --- .../Propagation/BaggagePropagatorTests.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index bdace7a83d6..9056555c885 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -290,26 +290,6 @@ public void ValidateOptionalWhiteSpaceBeforeSemicolonIgnored() Assert.Equal("SomeValue", baggage.Value); } - [Fact] - public void ValidatePercentEncoding() - { - var originalValue = "\t \"\';=asdf!@#$%^&*()"; - var encodedValue = Uri.EscapeDataString(originalValue); - - var carrier = new Dictionary - { - { BaggagePropagator.BaggageHeaderName, $"SomeKey={encodedValue}" }, - }; - - var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.Single(propagationContext.Baggage.GetBaggage()); - - var baggage = propagationContext.Baggage.GetBaggage().FirstOrDefault(); - - Assert.Equal("SomeKey", baggage.Key); - Assert.Equal(originalValue, baggage.Value); - } - [Fact] public void ValidateInvalidFormatSkipped() { From 40e6b5d674e39d5bf00a11c7de92423d1cb62901 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 13:37:04 -0700 Subject: [PATCH 11/52] incorrect tests --- .../Context/Propagation/BaggagePropagatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 9056555c885..ee0d20c124c 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -643,7 +643,7 @@ public void ValidateValidTcharInKeyIsAcceptedOnExtract(string specialChar) var key = $"key{specialChar}name"; var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, $"prefix{key}suffix=value" }, + { BaggagePropagator.BaggageHeaderName, $"{key}=value" }, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); @@ -724,7 +724,7 @@ public void ValidatePercentEncodedValueCharacterDecodesCorrectly(string encoded, { var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, $"key=prefix{encoded}suffix" }, + { BaggagePropagator.BaggageHeaderName, $"key={encoded}" }, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); From 8a709317e9f0fd5a43a7131d7a800d613c5d4576 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 9 Apr 2026 14:16:18 -0700 Subject: [PATCH 12/52] duplicate of ValidateSpecialCharsBaggageExtraction, replaced with more comprehensive test --- .../Propagation/BaggagePropagatorTests.cs | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index ee0d20c124c..a07b49007dc 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -124,45 +124,6 @@ public void ValidateLongBaggageExtraction() Assert.Equal(new string('x', 8186), array[0].Value); } - [Fact] - public void ValidateSpecialCharsBaggageExtraction() - { - var encodedKey = WebUtility.UrlEncode("key2"); - var encodedValue = WebUtility.UrlEncode("!x_x,x-x&x(x\");:"); - var escapedKey = Uri.EscapeDataString("key()3"); - var escapedValue = Uri.EscapeDataString("value()!&;:"); - - Assert.Equal("key2", encodedKey); - Assert.Equal("!x_x%2Cx-x%26x(x%22)%3B%3A", encodedValue); - Assert.Equal("key%28%293", escapedKey); - Assert.Equal("value%28%29%21%26%3B%3A", escapedValue); - - var initialBaggage = $"key+1=value+1,{encodedKey}={encodedValue},{escapedKey}={escapedValue}"; - var carrier = new List> - { - new KeyValuePair(BaggagePropagator.BaggageHeaderName, initialBaggage), - }; - - var propagationContext = this.baggage.Extract(default, carrier, GetterList); - - Assert.False(propagationContext == default); - Assert.True(propagationContext.ActivityContext == default); - - Assert.Equal(3, propagationContext.Baggage.Count); - - var actualBaggage = propagationContext.Baggage.GetBaggage(); - - Assert.Equal(3, actualBaggage.Count); - - Assert.True(actualBaggage.ContainsKey("key+1")); - Assert.Equal("value+1", actualBaggage["key+1"]); - - Assert.True(actualBaggage.ContainsKey("key2")); - Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]); - - Assert.True(!actualBaggage.ContainsKey("key()3")); - } - [Fact] public void ValidateEmptyBaggageInjection() { @@ -583,7 +544,7 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) [InlineData("key=val%2Bue", "key", "val+ue")] [InlineData("key=val%20ue", "key", "val ue")] [InlineData("key=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~", "key", " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] - public void ValidateMiscTests(string propagatedBaggage, string key, string expectedDecodedValue) + public void ValidateSpecialCharsBaggageExtraction(string propagatedBaggage, string key, string expectedDecodedValue) { var carrier = new List> { From fa34923f540370d00d4b8a4a4946789a20224136 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 14 Apr 2026 19:53:36 -0700 Subject: [PATCH 13/52] "," and "=" are being used as delimiters so tests should change accordingly. keys will automatically not have these delimiters --- .../Propagation/BaggagePropagatorTests.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 2515f58f20a..9c4d7067487 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -678,12 +678,10 @@ public void ValidateValidTcharInKeyIsAcceptedOnExtract(string specialChar) [InlineData("\"")] [InlineData("(")] [InlineData(")")] - [InlineData(",")] [InlineData("/")] [InlineData(":")] [InlineData(";")] [InlineData("<")] - [InlineData("=")] [InlineData(">")] [InlineData("?")] [InlineData("@")] @@ -708,6 +706,34 @@ public void ValidateKeyWithInvalidTcharDroppedOnExtract(string invalidChar) Assert.Equal("valid-key", propagationContext.Baggage.GetBaggage().First().Key); } + [Fact] + public void ValidateKeyWithValidDelimitersNotDroppedOnExtract() + { + var invalidKey = $"key,name"; + var carrier = new Dictionary + { + { + BaggagePropagator.BaggageHeaderName, + $"{invalidKey}=should-be-dropped,valid-key=valid-value" + }, + }; + + var propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Equal("name", propagationContext.Baggage.GetBaggage().First().Key); + + invalidKey = $"key=name"; + carrier = new Dictionary + { + { + BaggagePropagator.BaggageHeaderName, + $"{invalidKey}=should-be-dropped,valid-key=valid-value" + }, + }; + + propagationContext = this.baggage.Extract(default, carrier, Getter); + Assert.Equal("key", propagationContext.Baggage.GetBaggage().First().Key); + } + [Theory] [InlineData("%21", "!")] [InlineData("%22", "\"")] From 6dc924689a7e08247d0ab79f99401d7e6dee8342 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 15 Apr 2026 15:44:00 -0700 Subject: [PATCH 14/52] bad tests now fixed --- .../Propagation/BaggagePropagatorTests.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 9c4d7067487..56a841b71f5 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Net; using Xunit; namespace OpenTelemetry.Context.Propagation.Tests; @@ -545,18 +544,18 @@ public void KeyPlusInKeyPreservedLiterallyOnExtract() } [Theory] - [InlineData(" ")] - [InlineData("(")] - [InlineData(":")] - [InlineData(";")] - [InlineData("@")] - public void KeyWithDelimiterCharEntirePairDroppedOnInject(string delimiter) + [InlineData(" ", "%20")] + [InlineData("(", "%28")] + [InlineData(":", "%3A")] + [InlineData(";", "%3B")] + [InlineData("@", "%40")] + public void KeyWithDelimiterCharIsPercentEncodedOnInject(string delimiter, string expectedEncoding) { var propagationContext = new PropagationContext( default, new Baggage(new Dictionary { - { $"invalid{delimiter}key", "should-be-dropped" }, + { $"invalid{delimiter}key", "should-be-kept" }, { "valid-key", "valid-value" }, })); @@ -564,7 +563,13 @@ public void KeyWithDelimiterCharEntirePairDroppedOnInject(string delimiter) this.baggage.Inject(propagationContext, carrier, Setter); Assert.Single(carrier); - Assert.Equal("valid-key=valid-value", carrier[BaggagePropagator.BaggageHeaderName]); + + var header = carrier[BaggagePropagator.BaggageHeaderName]; + + Assert.Contains("valid-key=valid-value", header, StringComparison.Ordinal); + var expectedEncodedKey = $"invalid{expectedEncoding}key=should-be-kept"; + Assert.Contains(expectedEncodedKey, header, StringComparison.Ordinal); + Assert.Equal(2, header.Split(',').Length); } [Theory] @@ -855,7 +860,7 @@ public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() { var carrier = new Dictionary(); this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, { "invalid key", "should-be-dropped" }, })), carrier, Setter); + new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, })), carrier, Setter); var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); Assert.Single(extracted); From a5e729aee637cfa6d5829b0c4a94079e0cfece99 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 16 Apr 2026 13:05:17 -0700 Subject: [PATCH 15/52] [Baggage] Follow spec faithfully --- .../Context/Propagation/BaggagePropagator.cs | 86 +++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 8f354ecd3b4..180489e60f8 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -23,8 +23,27 @@ public class BaggagePropagator : TextMapPropagator private const int MaxBaggageLength = 8192; private const int MaxBaggageItems = 180; + private static readonly char[] InvalidCharsArray = + Enumerable.Range(0, 32).Select(c => (char)c) + .Concat(new[] { (char)127 }) + .Concat("()<>@,;:\\\"/[]?={} \t".ToCharArray()) + .Distinct() + .ToArray(); + + private static readonly char[] InvalidValueChars = + Enumerable.Range(0, 32).Select(c => (char)c) + .Concat(new[] { (char)127 }) + .Concat(new[] { ',', ';', '"', '\\', ' ' }) + .Distinct() + .ToArray(); + #if NET9_0_OR_GREATER - private static readonly SearchValues DecodeHints = SearchValues.Create('%', '+'); + private static readonly SearchValues DecodeHints = SearchValues.Create('%'); + + private static readonly SearchValues InvalidKeySearcher = SearchValues.Create(InvalidCharsArray); + + private static readonly SearchValues InvalidValueSearcher = + SearchValues.Create(InvalidValueChars); #endif /// @@ -108,8 +127,8 @@ public override void Inject(PropagationContext context, T carrier, Action 0) @@ -181,7 +200,13 @@ internal static bool TryExtractBaggage( continue; } - var key = DecodeIfNeeded(pair.Slice(0, separatorIndex)); + // Do not decode keys, add a function that checks if key-value pair needs to be dropped + if (!IsValidKey(pair.Slice(0, separatorIndex))) + { + continue; + } + + var key = pair.Slice(0, separatorIndex).ToString(); var value = DecodeIfNeeded(pair.Slice(separatorIndex + 1)); if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) @@ -213,10 +238,61 @@ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaini return result; } + private static string EncodeValue(ReadOnlySpan value) + { +#if NET9_0_OR_GREATER + if (!value.ContainsAny(InvalidValueSearcher)) + { + return value.ToString(); // fast path + } +#else + if (value.IndexOfAny(InvalidValueChars) < 0) + { + return value.ToString(); // fast path + } +#endif + + var sb = new StringBuilder(value.Length); + + foreach (var c in value) + { + if (IsInvalidValueChar(c)) + { + sb.Append('%') + .Append(((int)c).ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static bool IsInvalidValueChar(char c) + { + if (c <= 31 || c == 127) + { + return true; + } + + return c == ',' || c == ';' || c == '"' || c == '\\' || c == ' '; + } + + private static bool IsValidKey(ReadOnlySpan key) + { +#if NET9_0_OR_GREATER + return !key.ContainsAny(InvalidKeySearcher); +#else + return key.IndexOfAny(InvalidCharsArray) < 0; +#endif + } + private static string DecodeIfNeeded(ReadOnlySpan value) => #if NET9_0_OR_GREATER value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString(); #else - value.IndexOfAny('%', '+') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString()); + value.IndexOf('%') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString()); #endif } From 7ad665507208c46174337d44a54dc79236391b04 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 20 Apr 2026 15:14:54 -0700 Subject: [PATCH 16/52] https://www.w3.org/TR/baggage/#key --- .../Context/Propagation/BaggagePropagatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 56a841b71f5..5d873219502 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -173,7 +173,7 @@ public void ValidateOWSOnExtraction() { var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, "SomeKey \t = \t SomeValue \t , \t SomeKey2 \t = \t SomeValue2" }, + { BaggagePropagator.BaggageHeaderName, "SomeKey= \t SomeValue \t ,SomeKey2= \t SomeValue2" }, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); @@ -212,7 +212,7 @@ public void ValidateOptionalWhiteSpaceExtractionDoesNotCorruptOnReinjection() // Simulates a header emitted by .NET 10's W3C propagator var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, "correlationId = 12345, userId = user-abc" }, + { BaggagePropagator.BaggageHeaderName, "correlationId= 12345,userId= user-abc" }, }; var extractedContext = this.baggage.Extract(default, carrier, Getter); From 77a8f20d004786914c5f087b9791d7f7ec95b28b Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 20 Apr 2026 15:20:08 -0700 Subject: [PATCH 17/52] lint --- .../Context/Propagation/BaggagePropagatorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 5d873219502..22df5db4ce2 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -813,7 +813,7 @@ public void ValidateCharacterOutsideBaggageOctetIsPercentEncodedOnInject( { var propagationContext = new PropagationContext( default, - new Baggage(new Dictionary { { "key", $"val{rawChar}ue" } })); + new Baggage(new Dictionary { { "key", $"prefix{rawChar}suffix" } })); var carrier = new Dictionary(); this.baggage.Inject(propagationContext, carrier, Setter); From 27a4ac94f1a71083259a92371d1c23e30b5160a6 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 20 Apr 2026 15:24:25 -0700 Subject: [PATCH 18/52] all lint issues resolved --- .../Context/Propagation/BaggagePropagatorTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 22df5db4ce2..6d57087a8d2 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -589,7 +589,7 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) { // These characters are valid unencoded baggage-octets and must not be // transformed on either inject or extract. - var value = $"val{octet}ue"; + var value = $"val{octet}use"; var propagationContext = new PropagationContext( default, new Baggage(new Dictionary { { "key", value } })); @@ -607,8 +607,8 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) [Theory] [InlineData("key%201=value%201", "key%201", "value 1")] - [InlineData("key=val%2Bue", "key", "val+ue")] - [InlineData("key=val%20ue", "key", "val ue")] + [InlineData("key=val%2Buse", "key", "val+use")] + [InlineData("key=val%20use", "key", "val use")] [InlineData("key=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~", "key", " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")] public void ValidateSpecialCharsBaggageExtraction(string propagatedBaggage, string key, string expectedDecodedValue) { @@ -846,7 +846,7 @@ public void RoundTripValueWithSpacePreservedAsSpace() [Fact] public void RoundTripValueWithAllMandatoryEncodeCharsPreservedExactly() { - const string original = "val ue\"wi,th;back\\slash"; + const string original = "val use\"wi,th;back\\slash"; var carrier = new Dictionary(); this.baggage.Inject(new PropagationContext(default, new Baggage(new Dictionary { { "key", original }, })), carrier, Setter); From b95d627e4b118b98494bb78e8b7d74dfa70f5e83 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 20 Apr 2026 15:46:43 -0700 Subject: [PATCH 19/52] dotnet format errors --- .../Context/Propagation/BaggagePropagatorTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 6d57087a8d2..da813315155 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -859,8 +859,7 @@ public void RoundTripValueWithAllMandatoryEncodeCharsPreservedExactly() public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() { var carrier = new Dictionary(); - this.baggage.Inject( - new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, })), carrier, Setter); + this.baggage.Inject(new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, })), carrier, Setter); var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); Assert.Single(extracted); From efd1a11d5a0fc935f507aaf2b7dc24b951b31092 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 22 Apr 2026 09:11:37 -0700 Subject: [PATCH 20/52] [API] Refactor BaggagePropagator encoding methods and enhance fuzz tests for baggage normalization --- .../Context/Propagation/BaggagePropagator.cs | 34 ++++++- .../Propagation/BaggagePropagatorFuzzTests.cs | 90 ++++++++++++++++++- .../Context/Propagation/Generators.cs | 11 ++- 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 77f75a40eae..ea12f5b3db9 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -127,7 +127,7 @@ public override void Inject(PropagationContext context, T carrier, Action value) return sb.ToString(); } + private static string EncodeKey(ReadOnlySpan key) + { +#if NET9_0_OR_GREATER + if (!key.ContainsAny(InvalidKeySearcher)) + { + return key.ToString(); // fast path + } +#else + if (key.IndexOfAny(InvalidCharsArray) < 0) + { + return key.ToString(); // fast path + } +#endif + + var sb = new StringBuilder(key.Length); + + foreach (var c in key) + { + if (!IsValidKey(new ReadOnlySpan([c]))) + { + sb.Append('%') + .Append(((int)c).ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + private static bool IsInvalidValueChar(char c) { if (c <= 31 || c == 127) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs index d1008b63d13..5ba00fa41a6 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Net; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -28,7 +29,11 @@ public Property InjectExtractRoundTripPreservesSafeBaggage() => Prop.ForAll(Gene var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter); - return DictionariesEqual(baggageItems, extracted.Baggage.GetBaggage()); + var normalized = Normalize(baggageItems); + + return DictionariesEqual( + normalized, + extracted.Baggage.GetBaggage()); } catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) { @@ -144,4 +149,87 @@ private static Dictionary ExpectedBaggageForOversizedHeader(stri return expected; } + + private static Dictionary Normalize(Dictionary input) + { + var result = new Dictionary(StringComparer.Ordinal); + + foreach (KeyValuePair kvp in input) + { + var key = kvp.Key; + var value = kvp.Value; + + if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) + { + continue; + } + + if (!IsValidKey(key)) + { + continue; + } + +#if NET || NETSTANDARD2_1_OR_GREATER + var semicolonIndex = value.IndexOf(';', StringComparison.Ordinal); +#else + var semicolonIndex = value.IndexOf(';'); +#endif + + var truncated = semicolonIndex >= 0 + ? value.Substring(0, semicolonIndex) + : value; + + truncated = truncated.Trim(); + + var decoded = DecodeIfNeeded(truncated); + + if (string.IsNullOrEmpty(decoded)) + { + continue; + } + + result[key] = decoded; + } + + return result; + } + + private static string DecodeIfNeeded(string value) + { +#if NET || NETSTANDARD2_1_OR_GREATER + return value.Contains('%', StringComparison.Ordinal) == true + ? WebUtility.UrlDecode(value) + : value; +#else + return value.Contains('%') == true + ? WebUtility.UrlDecode(value) + : value; +#endif + } + + private static bool IsValidKey(string key) + { + foreach (var c in key) + { + // Control chars + if (c <= 31 || c == 127) + { + return false; + } + + // Delimiters disallowed in baggage keys + switch (c) + { + case '(': case ')': case '<': case '>': + case '@': case ',': case ';': case ':': + case '\\': case '"': case '/': case '[': + case ']': case '?': case '=': case '{': + case '}': case ' ': + case '\t': + return false; + } + } + + return true; + } } diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs index 1c4eff6c5e9..3bf33f38bb8 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs @@ -13,6 +13,13 @@ internal static class Generators private static readonly Gen TraceStateKeyChar = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789_-*/".ToCharArray()); private static readonly Gen TraceStateValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~:/".ToCharArray()); private static readonly Gen BaggageChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_./:!$&'()*+;@?=,".ToCharArray()); + + private static readonly Gen SafeBaggageKeyChar = Gen.Elements( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~".ToCharArray()); + + private static readonly Gen SafeBaggageValueChar = Gen.Elements( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()*+-./:<=>?@[]^_`{|}~".ToCharArray()); + private static readonly Gen CompactBaggageValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-".ToCharArray()); private static readonly Gen HeaderValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=,;: ./@".ToCharArray()); @@ -35,8 +42,8 @@ from traceState in Gen.OneOf( public static Arbitrary> SafeBaggageDictionaryArbitrary() { var pairGen = - from key in CreateString(BaggageChar, 1, 12) - from value in CreateString(BaggageChar, 1, 24) + from key in CreateString(SafeBaggageKeyChar, 1, 12) + from value in CreateString(SafeBaggageValueChar, 1, 24) select new KeyValuePair(key, value); var gen = Gen.Sized(size => From 3facd0ec51350320cc353c549e2c6c1c4e6c207b Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 22 Apr 2026 09:45:43 -0700 Subject: [PATCH 21/52] lint --- .../Propagation/BaggagePropagatorFuzzTests.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs index 5ba00fa41a6..fdeee548b00 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs @@ -220,11 +220,24 @@ private static bool IsValidKey(string key) // Delimiters disallowed in baggage keys switch (c) { - case '(': case ')': case '<': case '>': - case '@': case ',': case ';': case ':': - case '\\': case '"': case '/': case '[': - case ']': case '?': case '=': case '{': - case '}': case ' ': + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + case '{': + case '}': + case ' ': case '\t': return false; } From 2bbaa69225024021e8d2884bfa1c7864bae6ef06 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Thu, 23 Apr 2026 15:12:28 -0700 Subject: [PATCH 22/52] Apply suggestions from code review Co-authored-by: Martin Costello --- .../Context/Propagation/BaggagePropagator.cs | 10 ++++------ .../Context/Propagation/Generators.cs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index ea12f5b3db9..15f244bb4a4 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -43,7 +43,7 @@ public class BaggagePropagator : TextMapPropagator private static readonly SearchValues InvalidKeySearcher = SearchValues.Create(InvalidCharsArray); private static readonly SearchValues InvalidValueSearcher = - SearchValues.Create(InvalidValueChars); + SearchValues.Create(InvalidValueChars); #endif /// @@ -323,14 +323,12 @@ private static bool IsInvalidValueChar(char c) return c == ',' || c == ';' || c == '"' || c == '\\' || c == ' '; } - private static bool IsValidKey(ReadOnlySpan key) - { + private static bool IsValidKey(ReadOnlySpan key) => #if NET9_0_OR_GREATER - return !key.ContainsAny(InvalidKeySearcher); + key.ContainsAny(InvalidKeySearcher); #else - return key.IndexOfAny(InvalidCharsArray) < 0; + key.IndexOfAny(InvalidCharsArray) < 0; #endif - } private static string DecodeIfNeeded(ReadOnlySpan value) => #if NET9_0_OR_GREATER diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs index 3bf33f38bb8..3c835df8fc7 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs @@ -15,10 +15,10 @@ internal static class Generators private static readonly Gen BaggageChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_./:!$&'()*+;@?=,".ToCharArray()); private static readonly Gen SafeBaggageKeyChar = Gen.Elements( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~".ToCharArray()); + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~".ToCharArray()); private static readonly Gen SafeBaggageValueChar = Gen.Elements( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()*+-./:<=>?@[]^_`{|}~".ToCharArray()); + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()*+-./:<=>?@[]^_`{|}~".ToCharArray()); private static readonly Gen CompactBaggageValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-".ToCharArray()); private static readonly Gen HeaderValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=,;: ./@".ToCharArray()); From 8b1bfdd47ca80cb46c8a37fe8c30a2f60902848e Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Fri, 24 Apr 2026 16:18:13 -0700 Subject: [PATCH 23/52] [API] Improve character validation logic and re-use code wherever possible, performance fixes BaggagePropogatorFuzzTest changes left todo --- .../Context/Propagation/BaggagePropagator.cs | 111 +++++++++++------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 15f244bb4a4..3a76a052fd7 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #if NET -#if NET9_0_OR_GREATER +#if NET8_0_OR_GREATER using System.Buffers; #endif using System.Diagnostics.CodeAnalysis; @@ -23,27 +23,34 @@ public class BaggagePropagator : TextMapPropagator private const int MaxBaggageLength = 8192; private const int MaxBaggageItems = 180; - private static readonly char[] InvalidCharsArray = - Enumerable.Range(0, 32).Select(c => (char)c) - .Concat(new[] { (char)127 }) - .Concat("()<>@,;:\\\"/[]?={} \t".ToCharArray()) - .Distinct() - .ToArray(); +#if NET8_0_OR_GREATER + private static readonly SearchValues DecodeHints = SearchValues.Create("%"); - private static readonly char[] InvalidValueChars = - Enumerable.Range(0, 32).Select(c => (char)c) - .Concat(new[] { (char)127 }) - .Concat(new[] { ',', ';', '"', '\\', ' ' }) - .Distinct() - .ToArray(); + private static readonly SearchValues InvalidKeySearcher = SearchValues.Create( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F \"(),/:;<=>?@[\\]{}"); -#if NET9_0_OR_GREATER - private static readonly SearchValues DecodeHints = SearchValues.Create('%'); + private static readonly SearchValues InvalidValueSearcher = SearchValues.Create( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F \",;\\"); - private static readonly SearchValues InvalidKeySearcher = SearchValues.Create(InvalidCharsArray); +#else - private static readonly SearchValues InvalidValueSearcher = - SearchValues.Create(InvalidValueChars); + private static readonly char[] InvalidCharsArray = + [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', + '\x7F', ' ', '"', '(', ')', ',', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}', + ]; + + private static readonly char[] InvalidValueChars = + [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', + '\x7F', ' ', '"', ',', ';', '\\', + ]; #endif /// @@ -251,26 +258,36 @@ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaini private static string EncodeValue(ReadOnlySpan value) { -#if NET9_0_OR_GREATER +#if NET8_0_OR_GREATER if (!value.ContainsAny(InvalidValueSearcher)) { - return value.ToString(); // fast path + return value.ToString(); } #else if (value.IndexOfAny(InvalidValueChars) < 0) { - return value.ToString(); // fast path + return value.ToString(); } #endif + const string hex = "0123456789ABCDEF"; var sb = new StringBuilder(value.Length); - +#if NET + Span encoded = ['%', '\0', '\0']; +#endif foreach (var c in value) { if (IsInvalidValueChar(c)) { +#if NET + encoded[1] = hex[(c >> 4) & 0xF]; + encoded[2] = hex[c & 0xF]; + sb.Append(encoded); +#else sb.Append('%') - .Append(((int)c).ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + .Append(hex[(c >> 4) & 0xF]) + .Append(hex[c & 0xF]); +#endif } else { @@ -283,26 +300,36 @@ private static string EncodeValue(ReadOnlySpan value) private static string EncodeKey(ReadOnlySpan key) { -#if NET9_0_OR_GREATER +#if NET8_0_OR_GREATER if (!key.ContainsAny(InvalidKeySearcher)) { - return key.ToString(); // fast path + return key.ToString(); } #else if (key.IndexOfAny(InvalidCharsArray) < 0) { - return key.ToString(); // fast path + return key.ToString(); } #endif + const string hex = "0123456789ABCDEF"; var sb = new StringBuilder(key.Length); - +#if NET + Span encoded = ['%', '\0', '\0']; +#endif foreach (var c in key) { - if (!IsValidKey(new ReadOnlySpan([c]))) + if (!IsValidKey(c)) { +#if NET + encoded[1] = hex[(c >> 4) & 0xF]; + encoded[2] = hex[c & 0xF]; + sb.Append(encoded); +#else sb.Append('%') - .Append(((int)c).ToString("X2", System.Globalization.CultureInfo.InvariantCulture)); + .Append(hex[(c >> 4) & 0xF]) + .Append(hex[c & 0xF]); +#endif } else { @@ -313,25 +340,29 @@ private static string EncodeKey(ReadOnlySpan key) return sb.ToString(); } - private static bool IsInvalidValueChar(char c) - { - if (c <= 31 || c == 127) - { - return true; - } + private static bool IsInvalidValueChar(char c) => +#if NET8_0_OR_GREATER + InvalidValueSearcher.Contains(c); +#else + Array.IndexOf(InvalidValueChars, c) >= 0; +#endif - return c == ',' || c == ';' || c == '"' || c == '\\' || c == ' '; - } + private static bool IsValidKey(char c) => +#if NET8_0_OR_GREATER + !InvalidKeySearcher.Contains(c); +#else + Array.IndexOf(InvalidCharsArray, c) < 0; +#endif private static bool IsValidKey(ReadOnlySpan key) => -#if NET9_0_OR_GREATER - key.ContainsAny(InvalidKeySearcher); +#if NET8_0_OR_GREATER + !key.ContainsAny(InvalidKeySearcher); #else key.IndexOfAny(InvalidCharsArray) < 0; #endif private static string DecodeIfNeeded(ReadOnlySpan value) => -#if NET9_0_OR_GREATER +#if NET8_0_OR_GREATER value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString(); #else value.IndexOf('%') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString()); From 67046f8206a127beaf3e4ac2a513217ad05dc7d3 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Sat, 25 Apr 2026 17:49:53 -0700 Subject: [PATCH 24/52] Update src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs Co-authored-by: Martin Costello --- src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 3a76a052fd7..564819438e9 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #if NET -#if NET8_0_OR_GREATER +#if NET using System.Buffers; #endif using System.Diagnostics.CodeAnalysis; From da80928b61b88d481ca75da583d8b466fd340a9f Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Sat, 25 Apr 2026 18:01:20 -0700 Subject: [PATCH 25/52] Apply suggestion from @martincostello Co-authored-by: Martin Costello --- src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 564819438e9..3b47131c582 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -40,7 +40,8 @@ public class BaggagePropagator : TextMapPropagator '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', - '\x7F', ' ', '"', '(', ')', ',', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}', + '\x7F', ' ', '"', '(', ')', ',', '/', ':', ';', '<', + '=', '>', '?', '@', '[', '\\', ']', '{', '}', ]; private static readonly char[] InvalidValueChars = From ea117771065b352387e1c7d820b9efe7961afb4c Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Sat, 25 Apr 2026 18:03:36 -0700 Subject: [PATCH 26/52] Refactor preprocessor directives for BaggagePropagator use NET not NET_8_OR_GREATER --- .../Context/Propagation/BaggagePropagator.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 3b47131c582..0ca1ad5a14f 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -23,7 +23,7 @@ public class BaggagePropagator : TextMapPropagator private const int MaxBaggageLength = 8192; private const int MaxBaggageItems = 180; -#if NET8_0_OR_GREATER +#if NET private static readonly SearchValues DecodeHints = SearchValues.Create("%"); private static readonly SearchValues InvalidKeySearcher = SearchValues.Create( @@ -259,7 +259,7 @@ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaini private static string EncodeValue(ReadOnlySpan value) { -#if NET8_0_OR_GREATER +#if NET if (!value.ContainsAny(InvalidValueSearcher)) { return value.ToString(); @@ -301,7 +301,7 @@ private static string EncodeValue(ReadOnlySpan value) private static string EncodeKey(ReadOnlySpan key) { -#if NET8_0_OR_GREATER +#if NET if (!key.ContainsAny(InvalidKeySearcher)) { return key.ToString(); @@ -342,28 +342,28 @@ private static string EncodeKey(ReadOnlySpan key) } private static bool IsInvalidValueChar(char c) => -#if NET8_0_OR_GREATER +#if NET InvalidValueSearcher.Contains(c); #else Array.IndexOf(InvalidValueChars, c) >= 0; #endif private static bool IsValidKey(char c) => -#if NET8_0_OR_GREATER +#if NET !InvalidKeySearcher.Contains(c); #else Array.IndexOf(InvalidCharsArray, c) < 0; #endif private static bool IsValidKey(ReadOnlySpan key) => -#if NET8_0_OR_GREATER +#if NET !key.ContainsAny(InvalidKeySearcher); #else key.IndexOfAny(InvalidCharsArray) < 0; #endif private static string DecodeIfNeeded(ReadOnlySpan value) => -#if NET8_0_OR_GREATER +#if NET value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString(); #else value.IndexOf('%') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString()); From a85ceb36895dac9a596f70e49d5ff7057edd7898 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Sat, 25 Apr 2026 18:14:42 -0700 Subject: [PATCH 27/52] use stackalloc --- .../Context/Propagation/BaggagePropagator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 0ca1ad5a14f..e9526edfd1a 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -274,7 +274,8 @@ private static string EncodeValue(ReadOnlySpan value) const string hex = "0123456789ABCDEF"; var sb = new StringBuilder(value.Length); #if NET - Span encoded = ['%', '\0', '\0']; + Span encoded = stackalloc char[3]; + encoded[0] = '%'; #endif foreach (var c in value) { @@ -316,7 +317,8 @@ private static string EncodeKey(ReadOnlySpan key) const string hex = "0123456789ABCDEF"; var sb = new StringBuilder(key.Length); #if NET - Span encoded = ['%', '\0', '\0']; + Span encoded = stackalloc char[3]; + encoded[0] = '%'; #endif foreach (var c in key) { From f07a0578eb137f940aca8c33294a26d37bfa1f68 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 27 Apr 2026 11:47:39 -0700 Subject: [PATCH 28/52] Refactor BaggagePropagator encoding methods for improved key and value handling --- .../Context/Propagation/BaggagePropagator.cs | 59 ++++--------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index e9526edfd1a..225479475e5 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -194,7 +194,7 @@ internal static bool TryExtractBaggage( while (!remaining.IsEmpty) { var pair = ReadNextSegment(ref remaining, ','); - baggageLength += pair.Length + 1; // pair and comma + baggageLength += pair.Length + 1; if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems) { @@ -208,7 +208,6 @@ internal static bool TryExtractBaggage( continue; } - // Do not decode keys, add a function that checks if key-value pair needs to be dropped if (!IsValidKey(pair.Slice(0, separatorIndex))) { continue; @@ -257,15 +256,19 @@ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaini return result; } - private static string EncodeValue(ReadOnlySpan value) + private static string EncodeKey(ReadOnlySpan key) => Encode(key, isKey: true); + + private static string EncodeValue(ReadOnlySpan value) => Encode(value, isKey: false); + + private static string Encode(ReadOnlySpan value, bool isKey) { #if NET - if (!value.ContainsAny(InvalidValueSearcher)) + if (!value.ContainsAny(isKey ? InvalidKeySearcher : InvalidValueSearcher)) { return value.ToString(); } #else - if (value.IndexOfAny(InvalidValueChars) < 0) + if (value.IndexOfAny(isKey ? InvalidCharsArray : InvalidValueChars) < 0) { return value.ToString(); } @@ -279,50 +282,8 @@ private static string EncodeValue(ReadOnlySpan value) #endif foreach (var c in value) { - if (IsInvalidValueChar(c)) - { -#if NET - encoded[1] = hex[(c >> 4) & 0xF]; - encoded[2] = hex[c & 0xF]; - sb.Append(encoded); -#else - sb.Append('%') - .Append(hex[(c >> 4) & 0xF]) - .Append(hex[c & 0xF]); -#endif - } - else - { - sb.Append(c); - } - } - - return sb.ToString(); - } - - private static string EncodeKey(ReadOnlySpan key) - { -#if NET - if (!key.ContainsAny(InvalidKeySearcher)) - { - return key.ToString(); - } -#else - if (key.IndexOfAny(InvalidCharsArray) < 0) - { - return key.ToString(); - } -#endif - - const string hex = "0123456789ABCDEF"; - var sb = new StringBuilder(key.Length); -#if NET - Span encoded = stackalloc char[3]; - encoded[0] = '%'; -#endif - foreach (var c in key) - { - if (!IsValidKey(c)) + var shouldEncode = isKey ? !IsValidKey(c) : IsInvalidValueChar(c); + if (shouldEncode) { #if NET encoded[1] = hex[(c >> 4) & 0xF]; From a02fdd32c36dbfbfadab1ff5d793976770094ef9 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 27 Apr 2026 11:51:42 -0700 Subject: [PATCH 29/52] changelog updated --- src/OpenTelemetry.Api/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index f4d4ed0d773..dbd5361f88b 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Fix BaggagePropogator to correctly follow Key and Value Encoding rules as mentioned +the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). + [#7051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7051) + * Fix baggage and trace headers not respecting the maximum length in some cases. ([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061)) From e18b62b0ff4580932b6bc807807c12fd1b0b3afe Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 27 Apr 2026 12:21:23 -0700 Subject: [PATCH 30/52] lint --- src/OpenTelemetry.Api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 84443ce5577..31a394fd220 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -6,7 +6,7 @@ Notes](../../RELEASENOTES.md). ## Unreleased -* Fix BaggagePropogator to correctly follow Key and Value Encoding rules as mentioned +* Fix `BaggagePropogator` to correctly follow Key and Value Encoding rules as mentioned the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). [#7051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7051) From 7b9755c732313a9c1a475bac647e66cc888679fe Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Mon, 27 Apr 2026 12:22:23 -0700 Subject: [PATCH 31/52] spelling mistake --- src/OpenTelemetry.Api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 31a394fd220..1e68211d082 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -6,7 +6,7 @@ Notes](../../RELEASENOTES.md). ## Unreleased -* Fix `BaggagePropogator` to correctly follow Key and Value Encoding rules as mentioned +* Fix `BaggagePropagator` to correctly follow Key and Value Encoding rules as mentioned the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). [#7051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7051) From cfc42ae2d42e347f9fa5c2ce51b9b95a6fd6758b Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 28 Apr 2026 10:08:58 -0700 Subject: [PATCH 32/52] Update src/OpenTelemetry.Api/CHANGELOG.md Co-authored-by: Martin Costello --- src/OpenTelemetry.Api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index 1e68211d082..d99bf215052 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -7,7 +7,7 @@ Notes](../../RELEASENOTES.md). ## Unreleased * Fix `BaggagePropagator` to correctly follow Key and Value Encoding rules as mentioned -the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). + the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). [#7051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7051) ## 1.15.3 From 6b47a1b360b365e861c4134d75060456aba67593 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 28 Apr 2026 10:12:51 -0700 Subject: [PATCH 33/52] Update test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs Co-authored-by: Martin Costello --- .../Context/Propagation/BaggagePropagatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index da813315155..63b4b7b1c71 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -781,8 +781,8 @@ public void ValidatePercentEncodedValueCharacterDecodesCorrectly(string encoded, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.Single(propagationContext.Baggage.GetBaggage()); - Assert.Equal(expected, propagationContext.Baggage.GetBaggage().First().Value); + var baggage = Assert.Single(propagationContext.Baggage.GetBaggage()); + Assert.Equal(expected, baggage.Value); } [Theory] From 4649afed8872dfe263222150b8cd7ae8d8ae772d Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 28 Apr 2026 11:08:32 -0700 Subject: [PATCH 34/52] Change BaggagePropagator tests for clarity --- .../Propagation/BaggagePropagatorFuzzTests.cs | 132 ++++-------------- .../Propagation/BaggagePropagatorTests.cs | 32 ++--- 2 files changed, 41 insertions(+), 123 deletions(-) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs index fdeee548b00..1d06ad83895 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Net; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; @@ -17,28 +16,43 @@ public class BaggagePropagatorFuzzTests private const int MaxBaggageItems = 180; [Property(MaxTest = MaxTests)] - public Property InjectExtractRoundTripPreservesSafeBaggage() => Prop.ForAll(Generators.SafeBaggageDictionaryArbitrary(), (baggageItems) => + public Property ExtractNeverThrowsOnArbitraryInput() => Prop.ForAll(Generators.BaggageCarrierArbitrary(), (carrier) => + { + try + { + var propagator = new BaggagePropagator(); + propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter); + return true; + } + catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) + { + return true; + } + catch + { + return false; + } + }); + + [Property(MaxTest = MaxTests)] + public Property InjectNeverThrowsOnArbitraryInput() => Prop.ForAll(Generators.BaggageDictionaryArbitrary(), (baggageItems) => { try { var propagator = new BaggagePropagator(); var carrier = new Dictionary(StringComparer.Ordinal); var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems)); - propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter); - - var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter); - - var normalized = Normalize(baggageItems); - - return DictionariesEqual( - normalized, - extracted.Baggage.GetBaggage()); + return true; } catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) { return true; } + catch + { + return false; + } }); [Property(MaxTest = MaxTests)] @@ -149,100 +163,4 @@ private static Dictionary ExpectedBaggageForOversizedHeader(stri return expected; } - - private static Dictionary Normalize(Dictionary input) - { - var result = new Dictionary(StringComparer.Ordinal); - - foreach (KeyValuePair kvp in input) - { - var key = kvp.Key; - var value = kvp.Value; - - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) - { - continue; - } - - if (!IsValidKey(key)) - { - continue; - } - -#if NET || NETSTANDARD2_1_OR_GREATER - var semicolonIndex = value.IndexOf(';', StringComparison.Ordinal); -#else - var semicolonIndex = value.IndexOf(';'); -#endif - - var truncated = semicolonIndex >= 0 - ? value.Substring(0, semicolonIndex) - : value; - - truncated = truncated.Trim(); - - var decoded = DecodeIfNeeded(truncated); - - if (string.IsNullOrEmpty(decoded)) - { - continue; - } - - result[key] = decoded; - } - - return result; - } - - private static string DecodeIfNeeded(string value) - { -#if NET || NETSTANDARD2_1_OR_GREATER - return value.Contains('%', StringComparison.Ordinal) == true - ? WebUtility.UrlDecode(value) - : value; -#else - return value.Contains('%') == true - ? WebUtility.UrlDecode(value) - : value; -#endif - } - - private static bool IsValidKey(string key) - { - foreach (var c in key) - { - // Control chars - if (c <= 31 || c == 127) - { - return false; - } - - // Delimiters disallowed in baggage keys - switch (c) - { - case '(': - case ')': - case '<': - case '>': - case '@': - case ',': - case ';': - case ':': - case '\\': - case '"': - case '/': - case '[': - case ']': - case '?': - case '=': - case '{': - case '}': - case ' ': - case '\t': - return false; - } - } - - return true; - } } diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 63b4b7b1c71..0e3c2bcf8a9 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -477,9 +477,9 @@ public void ValidateSpecialCharactersInjectionForValue() var baggageHeader = carrier[BaggagePropagator.BaggageHeaderName]; - Assert.Contains("%09", baggageHeader, StringComparison.Ordinal); - Assert.Contains("%20", baggageHeader, StringComparison.Ordinal); - Assert.Contains("%22", baggageHeader, StringComparison.Ordinal); + Assert.Contains("%09", baggageHeader, StringComparison.Ordinal); // Tab + Assert.Contains("%20", baggageHeader, StringComparison.Ordinal); // Space + Assert.Contains("%22", baggageHeader, StringComparison.Ordinal); // Quote var extractedContext = this.baggage.Extract(default, carrier, Getter); var extractedBaggage = extractedContext.Baggage.GetBaggage(); @@ -488,7 +488,7 @@ public void ValidateSpecialCharactersInjectionForValue() } [Fact] - public void KeyValidTcharSymbolInjectedUnchanged() + public void KeyValidTokenCharSymbolInjectedUnchanged() { var propagationContext = new PropagationContext( default, @@ -510,8 +510,8 @@ public void KeyValidTcharSymbolInjectedUnchanged() // on the wire that happen to contain %-sequences or '+' must be treated as // literal token strings they are NOT decoded. // - // '%' is a valid tchar, so "key%20name" is a valid token whose name is - // literally "key%20name", not "key name". '+' is also a valid tchar. + // '%' is a valid token char, so "key%20name" is a valid token whose name is + // literally "key%20name", not "key name". '+' is also a valid token char. // ------------------------------------------------------------------------- [Fact] public void KeyPercentSequenceInKeyPreservedLiterallyOnExtract() @@ -601,8 +601,9 @@ public void ValueValidBaggageOctetCharPassesThroughUnchanged(string octet) Assert.Contains(value, carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); // Confirm it round-trips back unchanged - var extracted = this.baggage.Extract(default, carrier, Getter); - Assert.Equal(value, extracted.Baggage.GetBaggage()["key"]); + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + Assert.True(extracted.TryGetValue("key", out var extractedValue)); + Assert.Equal(value, extractedValue); } [Theory] @@ -633,7 +634,7 @@ public void ValidateSpecialCharsBaggageExtraction(string propagatedBaggage, stri } [Fact] - public void ValidatePlusInValueIsLiteralOnExtract() + public void ValidatePlusIsNotEncoded() { var carrier = new Dictionary { @@ -664,9 +665,8 @@ public void ValidatePlusInValueIsLiteralOnExtract() [InlineData("`")] [InlineData("|")] [InlineData("~")] - public void ValidateValidTcharInKeyIsAcceptedOnExtract(string specialChar) + public void ValidateValidTokenCharInKeyIsAcceptedOnExtract(string specialChar) { - // This test is for all tchar characters are valid. var key = $"key{specialChar}name"; var carrier = new Dictionary { @@ -695,7 +695,7 @@ public void ValidateValidTcharInKeyIsAcceptedOnExtract(string specialChar) [InlineData("]")] [InlineData("{")] [InlineData("}")] - public void ValidateKeyWithInvalidTcharDroppedOnExtract(string invalidChar) + public void ValidateKeyWithInvalidTokenCharDroppedOnExtract(string invalidChar) { var invalidKey = $"key{invalidChar}name"; var carrier = new Dictionary @@ -789,7 +789,7 @@ public void ValidatePercentEncodedValueCharacterDecodesCorrectly(string encoded, [InlineData("key=%")] [InlineData("key=%2")] [InlineData("key=%GG")] - public void ValidateMalformedPercentSequenceInValueIsHandledGracefully(string headerValue) + public void ValidateMalformedPercentSequenceInValueDoesNotThrow(string headerValue) { var carrier = new Dictionary { @@ -861,8 +861,8 @@ public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() var carrier = new Dictionary(); this.baggage.Inject(new PropagationContext(default, new Baggage(new Dictionary { { "valid-key", "valid-value" }, })), carrier, Setter); - var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); - Assert.Single(extracted); - Assert.Equal("valid-value", extracted["valid-key"]); + var entry = Assert.Single(this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage()); + Assert.Equal("valid-key", entry.Key); + Assert.Equal("valid-value", entry.Value); } } From c34395de4a7a1fc0aebe05f5144f429f42b06993 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 28 Apr 2026 11:49:45 -0700 Subject: [PATCH 35/52] CHanges to generators are no longer needed --- .../Context/Propagation/Generators.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs index 499a2817a77..4a4cb04506f 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs @@ -14,13 +14,6 @@ internal static class Generators private static readonly Gen TraceStateKeyChar = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789_-*/".ToCharArray()); private static readonly Gen TraceStateValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~:/".ToCharArray()); private static readonly Gen BaggageChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_./:!$&'()*+;@?=,".ToCharArray()); - - private static readonly Gen SafeBaggageKeyChar = Gen.Elements( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~".ToCharArray()); - - private static readonly Gen SafeBaggageValueChar = Gen.Elements( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'()*+-./:<=>?@[]^_`{|}~".ToCharArray()); - private static readonly Gen CompactBaggageValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-".ToCharArray()); private static readonly Gen HeaderValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=,;: ./@".ToCharArray()); @@ -43,8 +36,8 @@ from traceState in Gen.OneOf( public static Arbitrary> SafeBaggageDictionaryArbitrary() { var pairGen = - from key in CreateString(SafeBaggageKeyChar, 1, 12) - from value in CreateString(SafeBaggageValueChar, 1, 24) + from key in CreateString(BaggageChar, 1, 12) + from value in CreateString(BaggageChar, 1, 24) select new KeyValuePair(key, value); var gen = Gen.Sized(size => From d22c4b339d4e112ebb4f6c2688f21a74c33788db Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 28 Apr 2026 14:41:15 -0700 Subject: [PATCH 36/52] let it throw --- .../Propagation/BaggagePropagatorFuzzTests.cs | 38 ++++--------------- 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs index 1d06ad83895..48096b29b23 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs @@ -18,41 +18,19 @@ public class BaggagePropagatorFuzzTests [Property(MaxTest = MaxTests)] public Property ExtractNeverThrowsOnArbitraryInput() => Prop.ForAll(Generators.BaggageCarrierArbitrary(), (carrier) => { - try - { - var propagator = new BaggagePropagator(); - propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter); - return true; - } - catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) - { - return true; - } - catch - { - return false; - } + var propagator = new BaggagePropagator(); + propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter); + return true; }); [Property(MaxTest = MaxTests)] public Property InjectNeverThrowsOnArbitraryInput() => Prop.ForAll(Generators.BaggageDictionaryArbitrary(), (baggageItems) => { - try - { - var propagator = new BaggagePropagator(); - var carrier = new Dictionary(StringComparer.Ordinal); - var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems)); - propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter); - return true; - } - catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) - { - return true; - } - catch - { - return false; - } + var propagator = new BaggagePropagator(); + var carrier = new Dictionary(StringComparer.Ordinal); + var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems)); + propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter); + return true; }); [Property(MaxTest = MaxTests)] From 0f79e1a3c86fb4c0a14ce17c41a137724f2e293d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Tue, 5 May 2026 14:03:12 +0200 Subject: [PATCH 37/52] post merge fix --- src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 225479475e5..843d988da77 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -1,10 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET #if NET using System.Buffers; -#endif using System.Diagnostics.CodeAnalysis; #endif using System.Net; From 79134cda59023c98a53723f90365b35c9c14a66b Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 12:01:31 -0700 Subject: [PATCH 38/52] change tests to assert existence of issues with '+' encoding and white space with keys --- .../Context/Propagation/BaggagePropagatorTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 0e3c2bcf8a9..a930a8bdc8f 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -173,7 +173,7 @@ public void ValidateOWSOnExtraction() { var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, "SomeKey= \t SomeValue \t ,SomeKey2= \t SomeValue2" }, + { BaggagePropagator.BaggageHeaderName, "SomeKey = \t SomeValue \t , SomeKey2 = \t SomeValue2" }, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); @@ -638,7 +638,7 @@ public void ValidatePlusIsNotEncoded() { var carrier = new Dictionary { - { BaggagePropagator.BaggageHeaderName, "key=value+1" }, + { BaggagePropagator.BaggageHeaderName, "key=a+b%20c" }, }; var propagationContext = this.baggage.Extract(default, carrier, Getter); @@ -646,7 +646,7 @@ public void ValidatePlusIsNotEncoded() var entry = propagationContext.Baggage.GetBaggage().First(); Assert.Equal("key", entry.Key); - Assert.Equal("value+1", entry.Value); // '+' stays as '+', not ' ' + Assert.Equal("a+b c", entry.Value); // '+' stays as '+', not ' ' } [Theory] From 67d99324dd4adee4008e15fb618a29bd3e8bca53 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 12:04:07 -0700 Subject: [PATCH 39/52] test checks whether injected header does not exceed 8192 max --- .../Propagation/BaggagePropagatorTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index a930a8bdc8f..901c5e9b858 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -503,6 +503,43 @@ public void KeyValidTokenCharSymbolInjectedUnchanged() Assert.Equal("my.key-name_here=value", carrier[BaggagePropagator.BaggageHeaderName]); } + [Fact] + public void ValidateNonAsciiEncodingIsCountedAgainstByteLimitNotCharCount() + { + // 'é' encodes to %C3%A9 (6 wire bytes per char, not 1). + // 1000 'é' chars = 6000 wire bytes for the value alone. + // With key "k=" (2 bytes) + 6000 = 6002 bytes, well within 8192 chars + // but the CHARACTER count (1002) would have appeared safe under the old code. + // We construct a case where purely char-count-based accounting would allow + // a second entry through that pushes the header over 8192 bytes. + // key "a" (1) + "=" (1) + 1365 × "é" → 1365 × 6 = 8190 wire bytes = 8192 total + // key "b" = "c" must be rejected as it would push past the limit + var nonAsciiValue = new string('\u00E9', 1365); + + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary + { + { "a", nonAsciiValue }, + { "b", "c" }, + })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Single(carrier); + + var header = carrier[BaggagePropagator.BaggageHeaderName]; + + Assert.True( + header.Length <= 8192, + $"Injected header length {header.Length} exceeds maximum of 8192 bytes"); + + Assert.False( + header.Contains("b=c", StringComparison.Ordinal), + "Second entry should have been excluded to stay within the byte limit"); + } + // ------------------------------------------------------------------------- // Keys incorrect path // The current implementation URL-decodes keys on extract (#5479). From 69fe2ef81933d2849a8858bb96a780e60fe8b82a Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 12:11:50 -0700 Subject: [PATCH 40/52] tests for non-ascii encoding and raw percent in inject situations --- .../Propagation/BaggagePropagatorTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 901c5e9b858..7e68c0fd5d8 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -540,6 +540,36 @@ public void ValidateNonAsciiEncodingIsCountedAgainstByteLimitNotCharCount() "Second entry should have been excluded to stay within the byte limit"); } + [Fact] + public void ValidateRawPercentInValueIsEncodedOnInject() + { + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", "x%20y" } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + // The literal '%' must be encoded as %25 so the value is unambiguous on the wire + Assert.Contains("%2520", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + } + + [Fact] + public void RoundTripRawPercentInValuePreservesLiteralPercent() + { + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", "x%20y" } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + var entry = Assert.Single(extracted); + Assert.Equal("key", entry.Key); + Assert.Equal("x%20y", entry.Value); + } + // ------------------------------------------------------------------------- // Keys incorrect path // The current implementation URL-decodes keys on extract (#5479). @@ -902,4 +932,70 @@ public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() Assert.Equal("valid-key", entry.Key); Assert.Equal("valid-value", entry.Value); } + + // ------------------------------------------------------------------------- + // Non-ASCII encoding + // ------------------------------------------------------------------------- + + [Fact] + public void ValidateNonAsciiValueIsUtf8PercentEncodedOnInject() + { + // 'é' is U+00E9, UTF-8: 0xC3 0xA9 → %C3%A9 + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", "caf\u00E9" } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Contains("%C3%A9", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + Assert.DoesNotContain("é", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + } + + [Fact] + public void RoundTripNonAsciiValuePreservesOriginalString() + { + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", "caf\u00E9" } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + var entry = Assert.Single(extracted); + Assert.Equal("key", entry.Key); + Assert.Equal("caf\u00E9", entry.Value); + } + + [Theory] + [InlineData("\u00E9", "%C3%A9")] + [InlineData("\u4E2D", "%E4%B8%AD")] + [InlineData("\u00A3", "%C2%A3")] + public void ValidateNonAsciiUtf8EncodingIsCorrect(string character, string expectedEncoding) + { + var propagationContext = new PropagationContext( + default, + new Baggage(new Dictionary { { "key", character } })); + + var carrier = new Dictionary(); + this.baggage.Inject(propagationContext, carrier, Setter); + + Assert.Contains(expectedEncoding, carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + } + + [Fact] + public void ValidateNonAsciiInExtractedValueIsPreservedAfterPercentEncoding() + { + // Simulate a header that already has correctly UTF-8 percent-encoded non-ASCII + var carrier = new Dictionary + { + { BaggagePropagator.BaggageHeaderName, "key=%C3%A9" }, + }; + + var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); + var entry = Assert.Single(extracted); + Assert.Equal("key", entry.Key); + Assert.Equal("\u00E9", entry.Value); + } } From 459c57afddff68a77033d264bf605c27fd795aa3 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 14:00:19 -0700 Subject: [PATCH 41/52] malformed percent sequences should be replaced, check was previously only checking no exceptions --- .../Propagation/BaggagePropagatorTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 7e68c0fd5d8..982368b1a2e 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -853,20 +853,22 @@ public void ValidatePercentEncodedValueCharacterDecodesCorrectly(string encoded, } [Theory] - [InlineData("key=%")] - [InlineData("key=%2")] - [InlineData("key=%GG")] - public void ValidateMalformedPercentSequenceInValueDoesNotThrow(string headerValue) + [InlineData("key=%", "key", "\uFFFD")] + [InlineData("key=%2", "key", "\uFFFD")] + [InlineData("key=%GG", "key", "\uFFFD")] + public void ValidateMalformedPercentSequenceInValueIsReplacedWithReplacementCharacter( + string headerValue, string expectedKey, string expectedValue) { var carrier = new Dictionary { { BaggagePropagator.BaggageHeaderName, headerValue }, }; - var exception = Record.Exception(() => - this.baggage.Extract(default, carrier, Getter)); + var propagationContext = this.baggage.Extract(default, carrier, Getter); - Assert.Null(exception); + var entry = Assert.Single(propagationContext.Baggage.GetBaggage()); + Assert.Equal(expectedKey, entry.Key); + Assert.Equal(expectedValue, entry.Value); } [Theory] From 15136655c72ef70a34f7e8bc0a50c4f23df98f15 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 14:46:20 -0700 Subject: [PATCH 42/52] extract is dropping content after semicolon as metadata --- .../Context/Propagation/BaggagePropagatorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 982368b1a2e..f5eb0256b97 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -915,7 +915,7 @@ public void RoundTripValueWithSpacePreservedAsSpace() [Fact] public void RoundTripValueWithAllMandatoryEncodeCharsPreservedExactly() { - const string original = "val use\"wi,th;back\\slash"; + const string original = "val use\"wi,thback\\slash"; var carrier = new Dictionary(); this.baggage.Inject(new PropagationContext(default, new Baggage(new Dictionary { { "key", original }, })), carrier, Setter); From 68c43b3ac362a6667a47f16d16bc8202dff2db24 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 17:05:14 -0700 Subject: [PATCH 43/52] checking if no exceptions are thrown in fuzz tests --- .../EnvironmentVariableCarrierFuzzTests.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/EnvironmentVariableCarrierFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/EnvironmentVariableCarrierFuzzTests.cs index ed935becf46..5942fa04dca 100644 --- a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/EnvironmentVariableCarrierFuzzTests.cs +++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/EnvironmentVariableCarrierFuzzTests.cs @@ -51,23 +51,15 @@ public Property TraceContextRoundTripWorksWithEnvironmentVariableCarrier() => Pr }); [Property(MaxTest = MaxTests)] - public Property BaggageRoundTripWorksWithEnvironmentVariableCarrier() => Prop.ForAll(Generators.SafeBaggageDictionaryArbitrary(), baggageItems => + public Property EnvironmentVariableCarrierNeverThrowsWithBaggageRoundTrip() => Prop.ForAll(Generators.SafeBaggageDictionaryArbitrary(), baggageItems => { - try - { - var carrier = new Dictionary(StringComparer.Ordinal); - var propagator = new BaggagePropagator(); + var carrier = new Dictionary(StringComparer.Ordinal); + var propagator = new BaggagePropagator(); - propagator.Inject(new PropagationContext(default, Baggage.Create(baggageItems)), carrier, EnvironmentVariableCarrier.Set); - - var extracted = propagator.Extract(default, EnvironmentVariableCarrier.Capture(carrier), EnvironmentVariableCarrier.Get); + propagator.Inject(new PropagationContext(default, Baggage.Create(baggageItems)), carrier, EnvironmentVariableCarrier.Set); + propagator.Extract(default, EnvironmentVariableCarrier.Capture(carrier), EnvironmentVariableCarrier.Get); - return DictionariesEqual(baggageItems, extracted.Baggage.GetBaggage()); - } - catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex)) - { - return true; - } + return true; }); [Property(MaxTest = MaxTests)] From acea4bfc259993d4fa05f5c80cf521e75b53c662 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 17:06:05 -0700 Subject: [PATCH 44/52] incorrect test as space is invalid character in key, '+' encoding is removed --- .../Context/Propagation/EnvironmentVariableCarrierTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs index 3130fb661ec..609758359b1 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/EnvironmentVariableCarrierTests.cs @@ -240,7 +240,7 @@ public void BaggagePropagator_RoundTripsThroughEnvironmentVariableCarrier() { var baggage = Baggage.Create(new Dictionary { - ["key 1"] = "value 1", + ["key1"] = "value 1", // space is not a valid token char in keys refer to #7051 ["key2"] = "value2", }); @@ -252,7 +252,7 @@ public void BaggagePropagator_RoundTripsThroughEnvironmentVariableCarrier() var extracted = propagator.Extract(default, EnvironmentVariableCarrier.Capture(carrier), EnvironmentVariableCarrier.Get); - Assert.Equal("key+1=value+1,key2=value2", carrier["BAGGAGE"]); + Assert.Equal("key1=value%201,key2=value2", carrier["BAGGAGE"]); AssertBaggageEqual(baggage.GetBaggage(), extracted.Baggage.GetBaggage()); } From 16341f189e74c1807290393ceb9909bc1d3ec639 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 17:06:14 -0700 Subject: [PATCH 45/52] - use positive sets - custom decoder for value when extracting - key is trimmed before validity check --- .../Context/Propagation/BaggagePropagator.cs | 191 ++++++++++++++---- 1 file changed, 151 insertions(+), 40 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 843d988da77..4a0acf85197 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Diagnostics.CodeAnalysis; #endif -using System.Net; using System.Text; using OpenTelemetry.Internal; @@ -24,31 +23,35 @@ public class BaggagePropagator : TextMapPropagator #if NET private static readonly SearchValues DecodeHints = SearchValues.Create("%"); - private static readonly SearchValues InvalidKeySearcher = SearchValues.Create( - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F \"(),/:;<=>?@[\\]{}"); + private static readonly SearchValues ValidKeySearcher = SearchValues.Create( + "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); - private static readonly SearchValues InvalidValueSearcher = SearchValues.Create( - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F \",;\\"); + // W3C Baggage §3.3 baggage-octet, '%' excluded so raw '%' is always encoded as %25 + private static readonly SearchValues ValidValueSearcher = SearchValues.Create( + "!#$&'()*+-./:<=>?@[]^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}"); #else - private static readonly char[] InvalidCharsArray = + private static readonly char[] ValidKeyChars = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', - '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', - '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', - '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', - '\x7F', ' ', '"', '(', ')', ',', '/', ':', ';', '<', - '=', '>', '?', '@', '[', '\\', ']', '{', '}', + '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; - private static readonly char[] InvalidValueChars = + // baggage-octet minus %, so raw % is always encoded as %25 + private static readonly char[] ValidValueChars = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', - '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', - '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', - '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', - '\x7F', ' ', '"', ',', ';', '\\', + '!', '#', '$', '&', '\'', '(', ')', '*', '+', '-', '.', '/', ':', + '<', '=', '>', '?', '@', '[', ']', '^', '_', '`', '{', '|', '}', '~', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; #endif @@ -206,12 +209,14 @@ internal static bool TryExtractBaggage( continue; } - if (!IsValidKey(pair.Slice(0, separatorIndex))) + var rawKey = pair.Slice(0, separatorIndex).Trim(); + + if (!IsValidKey(rawKey)) { continue; } - var key = pair.Slice(0, separatorIndex).ToString(); + var key = rawKey.ToString(); var rawValue = pair.Slice(separatorIndex + 1); @@ -261,12 +266,23 @@ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaini private static string Encode(ReadOnlySpan value, bool isKey) { #if NET - if (!value.ContainsAny(isKey ? InvalidKeySearcher : InvalidValueSearcher)) + if (!value.ContainsAnyExcept(isKey ? ValidKeySearcher : ValidValueSearcher)) { return value.ToString(); } #else - if (value.IndexOfAny(isKey ? InvalidCharsArray : InvalidValueChars) < 0) + var validChars = isKey ? ValidKeyChars : ValidValueChars; + var allValid = true; + foreach (var c in value) + { + if (Array.IndexOf(validChars, c) < 0) + { + allValid = false; + break; + } + } + + if (allValid) { return value.ToString(); } @@ -277,21 +293,44 @@ private static string Encode(ReadOnlySpan value, bool isKey) #if NET Span encoded = stackalloc char[3]; encoded[0] = '%'; + Span utf8Buffer = stackalloc byte[4]; #endif foreach (var c in value) { - var shouldEncode = isKey ? !IsValidKey(c) : IsInvalidValueChar(c); + var shouldEncode = isKey ? !IsValidKey(c) : !IsValidValueChar(c); if (shouldEncode) { + if (!isKey && c > '\x7F') + { #if NET - encoded[1] = hex[(c >> 4) & 0xF]; - encoded[2] = hex[c & 0xF]; - sb.Append(encoded); + var byteCount = Encoding.UTF8.GetBytes(new ReadOnlySpan(in c), utf8Buffer); + foreach (var b in utf8Buffer[..byteCount]) + { + encoded[1] = hex[(b >> 4) & 0xF]; + encoded[2] = hex[b & 0xF]; + sb.Append(encoded); + } #else - sb.Append('%') - .Append(hex[(c >> 4) & 0xF]) - .Append(hex[c & 0xF]); + foreach (var b in Encoding.UTF8.GetBytes(c.ToString())) + { + sb.Append('%') + .Append(hex[(b >> 4) & 0xF]) + .Append(hex[b & 0xF]); + } #endif + } + else + { +#if NET + encoded[1] = hex[(c >> 4) & 0xF]; + encoded[2] = hex[c & 0xF]; + sb.Append(encoded); +#else + sb.Append('%') + .Append(hex[(c >> 4) & 0xF]) + .Append(hex[c & 0xF]); +#endif + } } else { @@ -302,31 +341,103 @@ private static string Encode(ReadOnlySpan value, bool isKey) return sb.ToString(); } - private static bool IsInvalidValueChar(char c) => + private static bool IsValidValueChar(char c) => #if NET - InvalidValueSearcher.Contains(c); + ValidValueSearcher.Contains(c); #else - Array.IndexOf(InvalidValueChars, c) >= 0; + Array.IndexOf(ValidValueChars, c) >= 0; #endif private static bool IsValidKey(char c) => #if NET - !InvalidKeySearcher.Contains(c); + ValidKeySearcher.Contains(c); #else - Array.IndexOf(InvalidCharsArray, c) < 0; + Array.IndexOf(ValidKeyChars, c) >= 0; #endif - private static bool IsValidKey(ReadOnlySpan key) => + private static bool IsValidKey(ReadOnlySpan key) + { #if NET - !key.ContainsAny(InvalidKeySearcher); + return !key.ContainsAnyExcept(ValidKeySearcher); #else - key.IndexOfAny(InvalidCharsArray) < 0; + foreach (var c in key) + { + if (Array.IndexOf(ValidKeyChars, c) < 0) + { + return false; + } + } + + return true; #endif + } - private static string DecodeIfNeeded(ReadOnlySpan value) => + private static string DecodeIfNeeded(ReadOnlySpan value) + { #if NET - value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString(); + if (!value.ContainsAny(DecodeHints)) + { + return value.ToString(); + } #else - value.IndexOf('%') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString()); + if (value.IndexOf('%') < 0) + { + return value.ToString(); + } #endif + + var sb = new StringBuilder(value.Length); + + var byteBuffer = new byte[value.Length]; + var byteCount = 0; + var i = 0; + + while (i < value.Length) + { + if (value[i] == '%') + { + if (i + 2 < value.Length && IsHexDigit(value[i + 1]) && IsHexDigit(value[i + 2])) + { + byteBuffer[byteCount++] = (byte)((HexDigitValue(value[i + 1]) << 4) | HexDigitValue(value[i + 2])); + i += 3; + } + else + { + // Malformed %XX token here consume the whole token (up to 3 chars) and emit one U+FFFD per W3C Baggage spec §3.3.1.3 + FlushByteBuffer(sb, byteBuffer, ref byteCount); + sb.Append('\uFFFD'); + i += Math.Min(3, value.Length - i); // ← here + } + } + else + { + FlushByteBuffer(sb, byteBuffer, ref byteCount); + sb.Append(value[i]); + i++; + } + } + + FlushByteBuffer(sb, byteBuffer, ref byteCount); + + return sb.ToString(); + } + + private static void FlushByteBuffer(StringBuilder sb, byte[] buffer, ref int count) + { + if (count == 0) + { + return; + } + + sb.Append(Encoding.UTF8.GetString(buffer, 0, count)); + count = 0; + } + + private static bool IsHexDigit(char c) => + (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + + private static int HexDigitValue(char c) => + c >= '0' && c <= '9' ? c - '0' : + c >= 'A' && c <= 'F' ? c - 'A' + 10 : + c - 'a' + 10; } From 1d525742769e73d9c8572d4f5d4d5173d39765d7 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 19:47:59 -0700 Subject: [PATCH 46/52] remove non-ascii characters from code --- .../Context/Propagation/BaggagePropagator.cs | 4 ++-- .../Context/Propagation/BaggagePropagatorTests.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 4a0acf85197..39641f48d3a 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -26,7 +26,7 @@ public class BaggagePropagator : TextMapPropagator private static readonly SearchValues ValidKeySearcher = SearchValues.Create( "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); - // W3C Baggage §3.3 baggage-octet, '%' excluded so raw '%' is always encoded as %25 + // W3C Baggage 3.3 baggage-octet, '%' excluded so raw '%' is always encoded as %25 private static readonly SearchValues ValidValueSearcher = SearchValues.Create( "!#$&'()*+-./:<=>?@[]^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}"); @@ -403,7 +403,7 @@ private static string DecodeIfNeeded(ReadOnlySpan value) } else { - // Malformed %XX token here consume the whole token (up to 3 chars) and emit one U+FFFD per W3C Baggage spec §3.3.1.3 + // Malformed %XX token here consume the whole token (up to 3 chars) and emit one U+FFFD per W3C Baggage spec 3.3.1.3 FlushByteBuffer(sb, byteBuffer, ref byteCount); sb.Append('\uFFFD'); i += Math.Min(3, value.Length - i); // ← here diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index f5eb0256b97..476ed9086d4 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -506,14 +506,14 @@ public void KeyValidTokenCharSymbolInjectedUnchanged() [Fact] public void ValidateNonAsciiEncodingIsCountedAgainstByteLimitNotCharCount() { - // 'é' encodes to %C3%A9 (6 wire bytes per char, not 1). - // 1000 'é' chars = 6000 wire bytes for the value alone. + // '\u00E9' encodes to %C3%A9 (6 wire bytes per char, not 1). + // 1000 '\u00E9' chars = 6000 wire bytes for the value alone. // With key "k=" (2 bytes) + 6000 = 6002 bytes, well within 8192 chars // but the CHARACTER count (1002) would have appeared safe under the old code. // We construct a case where purely char-count-based accounting would allow // a second entry through that pushes the header over 8192 bytes. - // key "a" (1) + "=" (1) + 1365 × "é" → 1365 × 6 = 8190 wire bytes = 8192 total - // key "b" = "c" must be rejected as it would push past the limit + // key "a" (1) + "=" (1) + 1365 x "\u00E9" -> 1365 x 6 = 8190 wire bytes = 8192 total + var nonAsciiValue = new string('\u00E9', 1365); var propagationContext = new PropagationContext( @@ -942,7 +942,7 @@ public void RoundTripMixedValidAndInvalidKeysOnlyValidKeysSurvive() [Fact] public void ValidateNonAsciiValueIsUtf8PercentEncodedOnInject() { - // 'é' is U+00E9, UTF-8: 0xC3 0xA9 → %C3%A9 + // '\u00E9' is U+00E9, UTF-8: 0xC3 0xA9 -> %C3%A9 var propagationContext = new PropagationContext( default, new Baggage(new Dictionary { { "key", "caf\u00E9" } })); @@ -951,7 +951,7 @@ public void ValidateNonAsciiValueIsUtf8PercentEncodedOnInject() this.baggage.Inject(propagationContext, carrier, Setter); Assert.Contains("%C3%A9", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); - Assert.DoesNotContain("é", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); + Assert.DoesNotContain("\u00E9", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); } [Fact] From decb79e1ec3f625ac7f24102dd375af06d124e74 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Tue, 5 May 2026 19:49:30 -0700 Subject: [PATCH 47/52] remove non-ascii characters --- src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 39641f48d3a..9e4076457f8 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -403,10 +403,9 @@ private static string DecodeIfNeeded(ReadOnlySpan value) } else { - // Malformed %XX token here consume the whole token (up to 3 chars) and emit one U+FFFD per W3C Baggage spec 3.3.1.3 FlushByteBuffer(sb, byteBuffer, ref byteCount); sb.Append('\uFFFD'); - i += Math.Min(3, value.Length - i); // ← here + i += Math.Min(3, value.Length - i); } } else From 4f3bf5cb4abdbfa76118479f85dbcf0b7037607d Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 6 May 2026 11:43:27 -0700 Subject: [PATCH 48/52] [API] Baggage: consolidate percent encoding logic in BaggagePropagator, simplify isHexDigit --- .../Context/Propagation/BaggagePropagator.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 9e4076457f8..bc5b69f1f6f 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -16,6 +16,7 @@ namespace OpenTelemetry.Context.Propagation; public class BaggagePropagator : TextMapPropagator { internal const string BaggageHeaderName = "baggage"; + private const string Hex = "0123456789ABCDEF"; private const int MaxBaggageLength = 8192; private const int MaxBaggageItems = 180; @@ -288,11 +289,8 @@ private static string Encode(ReadOnlySpan value, bool isKey) } #endif - const string hex = "0123456789ABCDEF"; var sb = new StringBuilder(value.Length); #if NET - Span encoded = stackalloc char[3]; - encoded[0] = '%'; Span utf8Buffer = stackalloc byte[4]; #endif foreach (var c in value) @@ -306,30 +304,18 @@ private static string Encode(ReadOnlySpan value, bool isKey) var byteCount = Encoding.UTF8.GetBytes(new ReadOnlySpan(in c), utf8Buffer); foreach (var b in utf8Buffer[..byteCount]) { - encoded[1] = hex[(b >> 4) & 0xF]; - encoded[2] = hex[b & 0xF]; - sb.Append(encoded); + AppendPercentEncoded(sb, b); } #else foreach (var b in Encoding.UTF8.GetBytes(c.ToString())) { - sb.Append('%') - .Append(hex[(b >> 4) & 0xF]) - .Append(hex[b & 0xF]); + AppendPercentEncoded(sb, b); } #endif } else { -#if NET - encoded[1] = hex[(c >> 4) & 0xF]; - encoded[2] = hex[c & 0xF]; - sb.Append(encoded); -#else - sb.Append('%') - .Append(hex[(c >> 4) & 0xF]) - .Append(hex[c & 0xF]); -#endif + AppendPercentEncoded(sb, (byte)c); } } else @@ -433,10 +419,15 @@ private static void FlushByteBuffer(StringBuilder sb, byte[] buffer, ref int cou } private static bool IsHexDigit(char c) => - (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + char.IsAsciiDigit(c) || c is (>= 'A' and <= 'F') or (>= 'a' and <= 'f'); private static int HexDigitValue(char c) => c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c - 'a' + 10; + + private static void AppendPercentEncoded(StringBuilder sb, byte b) => + sb.Append('%') + .Append(Hex[(b >> 4) & 0xF]) + .Append(Hex[b & 0xF]); } From 9045fd47b92b7ae9432a905fdb40a40bed7c1ed3 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 6 May 2026 12:55:47 -0700 Subject: [PATCH 49/52] lint and simplification --- .../Context/Propagation/BaggagePropagator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index bc5b69f1f6f..f9b4a287e37 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -422,12 +422,10 @@ private static bool IsHexDigit(char c) => char.IsAsciiDigit(c) || c is (>= 'A' and <= 'F') or (>= 'a' and <= 'f'); private static int HexDigitValue(char c) => - c >= '0' && c <= '9' ? c - '0' : - c >= 'A' && c <= 'F' ? c - 'A' + 10 : - c - 'a' + 10; + c <= '9' ? c - '0' : (c & 0x0f) + 9; private static void AppendPercentEncoded(StringBuilder sb, byte b) => sb.Append('%') - .Append(Hex[(b >> 4) & 0xF]) - .Append(Hex[b & 0xF]); + .Append(Hex[(b >> 4) & 0xF]) + .Append(Hex[b & 0xF]); } From 48f4c1f1eb1b10ced875819169f35565a508b442 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Wed, 6 May 2026 13:02:52 -0700 Subject: [PATCH 50/52] [API] Baggage: iterate over unicode values instead of chars, handles non-BMP characters like emoji's now --- .../Context/Propagation/BaggagePropagator.cs | 76 ++++++++++++++----- .../Propagation/BaggagePropagatorTests.cs | 11 ++- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index f9b4a287e37..533e73ba1e7 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -290,39 +290,79 @@ private static string Encode(ReadOnlySpan value, bool isKey) #endif var sb = new StringBuilder(value.Length); + #if NET Span utf8Buffer = stackalloc byte[4]; -#endif - foreach (var c in value) + foreach (var rune in value.EnumerateRunes()) { - var shouldEncode = isKey ? !IsValidKey(c) : !IsValidValueChar(c); - if (shouldEncode) + if (rune.IsAscii) { - if (!isKey && c > '\x7F') + var c = (char)rune.Value; + if (isKey ? !IsValidKey(c) : !IsValidValueChar(c)) { -#if NET - var byteCount = Encoding.UTF8.GetBytes(new ReadOnlySpan(in c), utf8Buffer); - foreach (var b in utf8Buffer[..byteCount]) + AppendPercentEncoded(sb, (byte)c); + } + else + { + sb.Append(c); + } + } + else + { + // Non-ASCII rune: always encode as UTF-8 bytes. + // This correctly handles non-BMP scalar values (emoji, etc.) + // because Rune represents the full codepoint, not a surrogate half. + var byteCount = rune.EncodeToUtf8(utf8Buffer); + foreach (var b in utf8Buffer[..byteCount]) + { + AppendPercentEncoded(sb, b); + } + } + } +#else + var i = 0; + while (i < value.Length) + { + var c = value[i]; + + if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1])) + { + // Non-BMP pair: encode both chars as one UTF-8 sequence. + // Passing the pair to Encoding.UTF8 produces the correct 4-byte result + // rather than two replacement characters. + foreach (var b in Encoding.UTF8.GetBytes(new string(new[] { c, value[i + 1] }))) + { + AppendPercentEncoded(sb, b); + } + + i += 2; + } + else + { + var shouldEncode = isKey ? !IsValidKey(c) : !IsValidValueChar(c); + if (shouldEncode) + { + if (c > '\x7F') { - AppendPercentEncoded(sb, b); + foreach (var b in Encoding.UTF8.GetBytes(c.ToString())) + { + AppendPercentEncoded(sb, b); + } } -#else - foreach (var b in Encoding.UTF8.GetBytes(c.ToString())) + else { - AppendPercentEncoded(sb, b); + AppendPercentEncoded(sb, (byte)c); } -#endif } else { - AppendPercentEncoded(sb, (byte)c); + sb.Append(c); } - } - else - { - sb.Append(c); + + i++; } } +#endif return sb.ToString(); } diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs index 476ed9086d4..1536be6bc7b 100644 --- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs +++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs @@ -954,12 +954,14 @@ public void ValidateNonAsciiValueIsUtf8PercentEncodedOnInject() Assert.DoesNotContain("\u00E9", carrier[BaggagePropagator.BaggageHeaderName], StringComparison.Ordinal); } - [Fact] - public void RoundTripNonAsciiValuePreservesOriginalString() + [Theory] + [InlineData("caf\u00E9")] + [InlineData("emoji-\U0001F600")] + public void RoundTripNonAsciiValuePreservesOriginalString(string value) { var propagationContext = new PropagationContext( default, - new Baggage(new Dictionary { { "key", "caf\u00E9" } })); + new Baggage(new Dictionary { { "key", value } })); var carrier = new Dictionary(); this.baggage.Inject(propagationContext, carrier, Setter); @@ -967,13 +969,14 @@ public void RoundTripNonAsciiValuePreservesOriginalString() var extracted = this.baggage.Extract(default, carrier, Getter).Baggage.GetBaggage(); var entry = Assert.Single(extracted); Assert.Equal("key", entry.Key); - Assert.Equal("caf\u00E9", entry.Value); + Assert.Equal(value, entry.Value); } [Theory] [InlineData("\u00E9", "%C3%A9")] [InlineData("\u4E2D", "%E4%B8%AD")] [InlineData("\u00A3", "%C2%A3")] + [InlineData("\U0001F600", "%F0%9F%98%80")] public void ValidateNonAsciiUtf8EncodingIsCorrect(string character, string expectedEncoding) { var propagationContext = new PropagationContext( From 9ccd2c14ecda918a8bf7f5f378d2631aaf90a6e9 Mon Sep 17 00:00:00 2001 From: Navya Sharma Date: Fri, 8 May 2026 11:06:03 -0700 Subject: [PATCH 51/52] following go implementation of inject and extract --- .../Context/Propagation/BaggagePropagator.cs | 7 ++++++- .../Context/Propagation/BaggagePropagatorTests.cs | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs index 533e73ba1e7..ea17c4e721a 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs @@ -137,7 +137,12 @@ public override void Inject(PropagationContext context, T carrier, Action Date: Wed, 20 May 2026 11:09:48 -0700 Subject: [PATCH 52/52] lint --- src/OpenTelemetry.Api/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index acf2c22db58..66c497594db 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -13,7 +13,7 @@ Notes](../../RELEASENOTES.md). * Fix `BaggagePropagator` to correctly follow Key and Value Encoding rules as mentioned the [W3C Baggage specification](https://www.w3.org/TR/baggage/#key-and-value-encoding). [#7051](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7051) - + * Update `TraceContextPropagator` to support the W3C randomness flag. ([#7301](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7301))