Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
91029d3
add tests that check spec compatibility
nabutabu Apr 8, 2026
6684fbc
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 8, 2026
10ae63c
linter and comments fixed
nabutabu Apr 8, 2026
c74ac9b
lint
nabutabu Apr 8, 2026
8d89a5e
key 3 should be dropped, no need to try and access the value
nabutabu Apr 9, 2026
2d5a39f
- ' ' character (space) should be replaced by
nabutabu Apr 9, 2026
60b1906
duplicate, already tested by ValidateValueWithMultipleEqualsPreserves…
nabutabu Apr 9, 2026
054d005
duplicate to ValidateValueWithMultipleEqualsPreservesEquals
nabutabu Apr 9, 2026
e30a7b4
duplicate of ValidateKeyWithInvalidTcharDroppedOnExtract
nabutabu Apr 9, 2026
479f686
duplicate of ValidateMiscTests
nabutabu Apr 9, 2026
858bff8
duplicate of ValidatePercentEncodedComplexCharactersDecodesCorrectly
nabutabu Apr 9, 2026
40e6b5d
incorrect tests
nabutabu Apr 9, 2026
8a70931
duplicate of ValidateSpecialCharsBaggageExtraction, replaced with mor…
nabutabu Apr 9, 2026
1ecbbd1
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 14, 2026
fa34923
"," and "=" are being used as delimiters so tests should change accor…
nabutabu Apr 15, 2026
6dc9246
bad tests now fixed
nabutabu Apr 15, 2026
a5e729a
[Baggage] Follow spec faithfully
nabutabu Apr 16, 2026
8aa640d
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 16, 2026
b46f2bc
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 20, 2026
7ad6655
https://www.w3.org/TR/baggage/#key
nabutabu Apr 20, 2026
77a8f20
lint
nabutabu Apr 20, 2026
27a4ac9
all lint issues resolved
nabutabu Apr 20, 2026
b95d627
dotnet format errors
nabutabu Apr 20, 2026
efd1a11
[API] Refactor BaggagePropagator encoding methods and enhance fuzz te…
nabutabu Apr 22, 2026
3facd0e
lint
nabutabu Apr 22, 2026
2bbaa69
Apply suggestions from code review
nabutabu Apr 23, 2026
8b1bfdd
[API] Improve character validation logic and re-use code wherever pos…
nabutabu Apr 24, 2026
67046f8
Update src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
nabutabu Apr 26, 2026
da80928
Apply suggestion from @martincostello
nabutabu Apr 26, 2026
ea11777
Refactor preprocessor directives for BaggagePropagator use NET not NE…
nabutabu Apr 26, 2026
a85ceb3
use stackalloc
nabutabu Apr 26, 2026
f07a057
Refactor BaggagePropagator encoding methods for improved key and valu…
nabutabu Apr 27, 2026
a02fdd3
changelog updated
nabutabu Apr 27, 2026
f21a77a
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 27, 2026
e18b62b
lint
nabutabu Apr 27, 2026
7b9755c
spelling mistake
nabutabu Apr 27, 2026
a8e0b3c
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu Apr 27, 2026
cfc42ae
Update src/OpenTelemetry.Api/CHANGELOG.md
nabutabu Apr 28, 2026
6b47a1b
Update test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropag…
nabutabu Apr 28, 2026
4649afe
Change BaggagePropagator tests for clarity
nabutabu Apr 28, 2026
c34395d
CHanges to generators are no longer needed
nabutabu Apr 28, 2026
d22c4b3
let it throw
nabutabu Apr 28, 2026
775b025
Merge branch 'main' into mulitple-baggage-spec-reconciles
Kielek May 5, 2026
0f79e1a
post merge fix
Kielek May 5, 2026
79134cd
change tests to assert existence of issues with '+' encoding and whit…
nabutabu May 5, 2026
67d9932
test checks whether injected header does not exceed 8192 max
nabutabu May 5, 2026
69fe2ef
tests for non-ascii encoding and raw percent in inject situations
nabutabu May 5, 2026
459c57a
malformed percent sequences should be replaced, check was previously …
nabutabu May 5, 2026
1513665
extract is dropping content after semicolon as metadata
nabutabu May 5, 2026
68c43b3
checking if no exceptions are thrown in fuzz tests
nabutabu May 6, 2026
acea4bf
incorrect test as space is invalid character in key, '+' encoding is …
nabutabu May 6, 2026
16341f1
- use positive sets
nabutabu May 6, 2026
1d52574
remove non-ascii characters from code
nabutabu May 6, 2026
decb79e
remove non-ascii characters
nabutabu May 6, 2026
cb8c237
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu May 6, 2026
4f3bf5c
[API] Baggage: consolidate percent encoding logic in BaggagePropagato…
nabutabu May 6, 2026
31e1fd7
Merge branch 'mulitple-baggage-spec-reconciles' of github.com:nabutab…
nabutabu May 6, 2026
9045fd4
lint and simplification
nabutabu May 6, 2026
48f4c1f
[API] Baggage: iterate over unicode values instead of chars, handles …
nabutabu May 6, 2026
7f67044
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu May 6, 2026
9ccd2c1
following go implementation of inject and extract
nabutabu May 8, 2026
bdc4b01
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu May 8, 2026
76e5dd8
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu May 14, 2026
f3b92fb
Merge branch 'main' into mulitple-baggage-spec-reconciles
nabutabu May 19, 2026
a88b298
lint
nabutabu May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/OpenTelemetry.Api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Notes](../../RELEASENOTES.md).
Add support for using environment variables as context propagation carriers.
([#7174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7174))

* 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))

Expand Down
268 changes: 255 additions & 13 deletions src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

#if NET
#if NET9_0_OR_GREATER
using System.Buffers;
#endif
using System.Diagnostics.CodeAnalysis;
#endif
using System.Net;
using System.Text;
using OpenTelemetry.Internal;

Expand All @@ -19,12 +16,44 @@ 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;

#if NET9_0_OR_GREATER
private static readonly SearchValues<char> DecodeHints = SearchValues.Create('%', '+');
#if NET
private static readonly SearchValues<char> DecodeHints = SearchValues.Create("%");

private static readonly SearchValues<char> ValidKeySearcher = SearchValues.Create(
"!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");

// W3C Baggage 3.3 baggage-octet, '%' excluded so raw '%' is always encoded as %25
private static readonly SearchValues<char> ValidValueSearcher = SearchValues.Create(
"!#$&'()*+-./:<=>?@[]^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}");

#else

private static readonly char[] ValidKeyChars =
[
'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~',
'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',
];

// baggage-octet minus %, so raw % is always encoded as %25
private static readonly char[] ValidValueChars =
[
'!', '#', '$', '&', '\'', '(', ')', '*', '+', '-', '.', '/', ':',
'<', '=', '>', '?', '@', '[', ']', '^', '_', '`', '{', '|', '}', '~',
'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

/// <inheritdoc/>
Expand Down Expand Up @@ -108,8 +137,13 @@ public override void Inject<T>(PropagationContext context, T carrier, Action<T,
continue;
}

var encodedKey = WebUtility.UrlEncode(item.Key);
var encodedValue = WebUtility.UrlEncode(item.Value);
if (!IsValidKey(item.Key))
{
continue;
}

var encodedKey = item.Key;
var encodedValue = EncodeValue(item.Value);
var baggageItemLength = encodedKey.Length + encodedValue.Length + 1;

if (baggage.Length > 0)
Expand Down Expand Up @@ -167,7 +201,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)
{
Expand All @@ -183,6 +217,13 @@ internal static bool TryExtractBaggage(

var rawKey = pair.Slice(0, separatorIndex).Trim();

if (!IsValidKey(rawKey))
{
continue;
}

var key = rawKey.ToString();

var rawValue = pair.Slice(separatorIndex + 1);

var semicolonIndex = rawValue.IndexOf(';');
Expand All @@ -193,7 +234,6 @@ internal static bool TryExtractBaggage(

rawValue = rawValue.Trim();

var key = DecodeIfNeeded(rawKey);
var value = DecodeIfNeeded(rawValue);

if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
Expand Down Expand Up @@ -225,10 +265,212 @@ private static ReadOnlySpan<char> ReadNextSegment(ref ReadOnlySpan<char> remaini
return result;
}

private static string DecodeIfNeeded(ReadOnlySpan<char> value) =>
#if NET9_0_OR_GREATER
value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString();
private static string EncodeKey(ReadOnlySpan<char> key) => Encode(key, isKey: true);

private static string EncodeValue(ReadOnlySpan<char> value) => Encode(value, isKey: false);

private static string Encode(ReadOnlySpan<char> value, bool isKey)
{
#if NET
if (!value.ContainsAnyExcept(isKey ? ValidKeySearcher : ValidValueSearcher))
{
return value.ToString();
}
#else
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();
}
#endif

var sb = new StringBuilder(value.Length);

#if NET
Span<byte> utf8Buffer = stackalloc byte[4];
foreach (var rune in value.EnumerateRunes())
{
if (rune.IsAscii)
{
var c = (char)rune.Value;
if (isKey ? !IsValidKey(c) : !IsValidValueChar(c))
{
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')
{
foreach (var b in Encoding.UTF8.GetBytes(c.ToString()))
{
AppendPercentEncoded(sb, b);
}
}
else
{
AppendPercentEncoded(sb, (byte)c);
}
}
else
{
sb.Append(c);
}

i++;
}
}
#endif

return sb.ToString();
}

private static bool IsValidValueChar(char c) =>
#if NET
ValidValueSearcher.Contains(c);
#else
Array.IndexOf(ValidValueChars, c) >= 0;
#endif

private static bool IsValidKey(char c) =>
#if NET
ValidKeySearcher.Contains(c);
#else
Array.IndexOf(ValidKeyChars, c) >= 0;
#endif

private static bool IsValidKey(ReadOnlySpan<char> key)
{
#if NET
return !key.ContainsAnyExcept(ValidKeySearcher);
#else
value.IndexOfAny('%', '+') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString());
foreach (var c in key)
{
if (Array.IndexOf(ValidKeyChars, c) < 0)
{
return false;
}
}

return true;
#endif
}

private static string DecodeIfNeeded(ReadOnlySpan<char> value)
{
#if NET
if (!value.ContainsAny(DecodeHints))
{
return value.ToString();
}
#else
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
{
FlushByteBuffer(sb, byteBuffer, ref byteCount);
sb.Append('\uFFFD');
i += Math.Min(3, value.Length - i);
}
}
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) =>
char.IsAsciiDigit(c) || c is (>= 'A' and <= 'F') or (>= 'a' and <= 'f');

private static int HexDigitValue(char c) =>
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]);
}
Comment thread
martincostello marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,21 @@ 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();
var carrier = new Dictionary<string, string>(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 propagator = new BaggagePropagator();
propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
return true;
});

return DictionariesEqual(baggageItems, extracted.Baggage.GetBaggage());
}
catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
{
return true;
}
Comment thread
martincostello marked this conversation as resolved.
[Property(MaxTest = MaxTests)]
public Property InjectNeverThrowsOnArbitraryInput() => Prop.ForAll(Generators.BaggageDictionaryArbitrary(), (baggageItems) =>
{
var propagator = new BaggagePropagator();
var carrier = new Dictionary<string, string>(StringComparer.Ordinal);
var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems));
propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter);
return true;
});

[Property(MaxTest = MaxTests)]
Expand Down
Loading
Loading