Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
05a5c11
Re-introducing max baggage length
XSAM Apr 17, 2026
128f3c1
Fix lint
XSAM Apr 17, 2026
c274c8e
Update CHANGELOG
XSAM Apr 17, 2026
5eb9558
Merge branch 'main' into fix/baggage-parsing
XSAM Apr 17, 2026
f3bcf1a
Fix CHANGELOG
XSAM Apr 17, 2026
1bf7210
Update CHANGELOG.md
XSAM Apr 21, 2026
6feee46
Apply suggestion
XSAM Apr 22, 2026
4030ecd
Remove hardcoded 8192 in test cases
XSAM Apr 22, 2026
f155676
Enforce maxParseErrors for extractMultiBaggage
XSAM Apr 22, 2026
64cf1f1
Merge branch 'main' into fix/baggage-parsing
XSAM Apr 22, 2026
9ea953d
Fix linter
XSAM Apr 22, 2026
98da4b3
Merge branch 'fix/baggage-parsing' of github.com:XSAM/opentelemetry-g…
XSAM Apr 22, 2026
55e5b4f
Fix markdown
XSAM Apr 22, 2026
285423e
Update baggage/baggage.go
XSAM Apr 28, 2026
35b72da
Merge branch 'main' of github.com:open-telemetry/opentelemetry-go int…
XSAM Apr 28, 2026
1c8caf8
Count comma separator between combined header values
XSAM Apr 28, 2026
832c87b
Fix linter
XSAM Apr 28, 2026
1424a1c
Update propagation/baggage_test.go
XSAM Apr 28, 2026
df9b633
Apply suggestions
XSAM Apr 29, 2026
0f14624
Merge branch 'fix/baggage-parsing' of github.com:XSAM/opentelemetry-g…
XSAM Apr 29, 2026
3bdee54
Merge branch 'main' into fix/baggage-parsing
XSAM Apr 29, 2026
36456d1
Apply suggestions
XSAM May 1, 2026
d9d8cd2
Merge branch 'main' into fix/baggage-parsing
XSAM May 1, 2026
965ba68
Fix CHANGELOG format
XSAM May 1, 2026
c782fa6
Merge branch 'main' into fix/baggage-parsing
pellared May 4, 2026
f8daa64
Update comment for Parse method
XSAM May 8, 2026
b11ec9b
Count one parse error per failing header in extractMultiBaggage
XSAM May 8, 2026
b7a41f7
Return empty when overall header size exceed the 8192 bytes
XSAM May 8, 2026
e6e11ed
Merge branch 'main' into fix/baggage-parsing
XSAM May 8, 2026
dbe8855
Update baggage_test.go
XSAM May 8, 2026
84bb8b4
Merge branch 'main' into fix/baggage-parsing
XSAM May 12, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8152)
- `go.opentelemetry.io/otel/exporters/prometheus` now uses `Value.String` formatting for label values following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8170)
- Propagate errors from the exporter when calling `Shutdown` on `BatchSpanProcessor` in `go.opentelemetry.io/otel/sdk/trace`. (#8197)
- Optimize CPU and memory usage for baggage parsing in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`. (#8222)
Comment thread
XSAM marked this conversation as resolved.
Outdated

<!-- Released section -->
<!-- Don't change this section unless doing release -->
Expand Down
15 changes: 14 additions & 1 deletion baggage/baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
const (
maxMembers = 64
maxBytesPerBaggageString = 8192
maxParseErrors = 5

listDelimiter = ","
keyValueDelimiter = "="
Expand Down Expand Up @@ -504,9 +505,14 @@ func Parse(bStr string) (Baggage, error) {
return Baggage{}, nil
}

if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
Comment thread
XSAM marked this conversation as resolved.

b := make(baggage.List)
sizes := make(map[string]int) // Track per-key byte sizes
var totalBytes int
var parseErrors int
var truncateErr error
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
// Check member count limit.
Expand All @@ -517,7 +523,10 @@ func Parse(bStr string) (Baggage, error) {

m, err := parseMember(memberStr)
if err != nil {
truncateErr = errors.Join(truncateErr, err)
parseErrors++
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
Comment thread
XSAM marked this conversation as resolved.
continue // skip invalid member, keep processing
}

Expand Down Expand Up @@ -553,6 +562,10 @@ func Parse(bStr string) (Baggage, error) {
totalBytes = newTotalBytes
}

if dropped := parseErrors - maxParseErrors; dropped > 0 {
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more invalid members", dropped))
}

if len(b) == 0 {
return Baggage{}, truncateErr
}
Expand Down
58 changes: 43 additions & 15 deletions baggage/baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,7 @@ func TestBaggageParse(t *testing.T) {
{
name: "invalid baggage string: too large",
in: tooLarge,
// tooLarge is a single key without "=", so parseMember fails
err: errInvalidMember,
err: errBaggageBytes,
},
{
name: "baggage string with too many members keeps first 64",
Expand All @@ -544,31 +543,26 @@ func TestBaggageParse(t *testing.T) {
err: errMemberNumber,
},
{
name: "baggage string exceeds byte limit returns partial result",
name: "baggage string at max size is accepted",
in: func() string {
// Create members that collectively exceed maxBytesPerBaggageString.
// Each member: "kN=" + value. We use values large enough that
// a few members fit but the total exceeds 8192 bytes.
// Create a baggage string of exactly maxBytesPerBaggageString.
// 3 members: "k0=" + 2727v + "," + "k1=" + 2727v + "," + "k2=" + 2727v
// = 3*(3+2727) + 2 = 8192
val := strings.Repeat("v", 2727)
var parts []string
val := strings.Repeat("v", 2000)
for i := range 10 {
for i := range 3 {
parts = append(parts, fmt.Sprintf("k%d=%s", i, val))
}
return strings.Join(parts, ",")
}(),
want: func() baggage.List {
// Only members that fit within 8192 bytes should be kept.
// Each member is ~2003 bytes ("kN=" + 2000 "v"s), plus comma.
// 4 members = 4*2003 + 3 commas = 8015 bytes (fits).
// 5 members = 5*2003 + 4 commas = 10019 bytes (exceeds).
b := make(baggage.List)
val := strings.Repeat("v", 2000)
for i := range 4 {
val := strings.Repeat("v", 2727)
for i := range 3 {
b[fmt.Sprintf("k%d", i)] = baggage.Item{Value: val}
}
return b
}(),
err: errBaggageBytes,
},
{
name: "percent-encoded octet sequences do not match the UTF-8 encoding scheme",
Expand Down Expand Up @@ -1272,3 +1266,37 @@ func BenchmarkMemberString(b *testing.B) {
_ = member.String()
}
}

func BenchmarkParseOversized(b *testing.B) {
// 1MB oversized baggage string.
oversized := strings.Repeat("k=v,", 250000)

b.ReportAllocs()

for b.Loop() {
benchBaggage, _ = Parse(oversized)
Comment thread
pellared marked this conversation as resolved.
Outdated
}
}

func TestParseErrorCap(t *testing.T) {
// Build a baggage string with many invalid members (no '=' delimiter).
// All within the 8192 byte limit.
var parts []string
for i := range 20 {
parts = append(parts, fmt.Sprintf("bad%d", i))
}
// Add one valid member so the baggage is not empty.
parts = append(parts, "good=val")
bStr := strings.Join(parts, ",")

b, err := Parse(bStr)
assert.ErrorIs(t, err, errInvalidMember)
assert.Equal(t, 1, b.Len(), "should return the valid member")

// Count the number of joined errors.
errs := err.Error()
invalidCount := strings.Count(errs, "invalid baggage list-member")
assert.Equal(t, maxParseErrors, invalidCount,
"should cap individual parse errors at maxParseErrors")
assert.Contains(t, errs, "and 15 more invalid members")
}
9 changes: 8 additions & 1 deletion propagation/baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const (

// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxMembers = 64
maxBytesPerBaggageString = 8192
)

// Baggage is a propagator that supports the W3C Baggage format.
Expand Down Expand Up @@ -72,7 +73,13 @@ func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.C
}

var members []baggage.Member
var totalBytes int
for _, bStr := range bVals {
totalBytes += len(bStr)
Comment thread
pellared marked this conversation as resolved.
if totalBytes > maxBytesPerBaggageString {
break
}
Comment thread
XSAM marked this conversation as resolved.
Outdated
Comment thread
XSAM marked this conversation as resolved.

currBag, err := baggage.Parse(bStr)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
Expand Down
33 changes: 31 additions & 2 deletions propagation/baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
wantMaxBytes: maxBytesPerBaggageString,
},
{
name: "skips large member that exceeds byte limit and continues",
name: "stops processing when aggregate byte budget exceeded",
headers: []string{
"small1=v1,small2=v2",
"large=" + strings.Repeat("x", maxBytesPerBaggageString),
Expand All @@ -306,7 +306,6 @@ func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
want: members{
{Key: "small1", Value: "v1"},
{Key: "small2", Value: "v2"},
{Key: "small3", Value: "v3"},
},
},
}
Expand Down Expand Up @@ -498,3 +497,33 @@ func TestBaggagePropagatorGetAllKeys(t *testing.T) {
t.Errorf("GetAllKeys: -got +want %s", diff)
}
}

func TestExtractOversizedSingleBaggageHeader(t *testing.T) {
prop := propagation.Baggage{}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody)
// Set a single baggage header exceeding 8192 bytes.
req.Header.Set("baggage", "key="+strings.Repeat("v", 8192))

Comment thread
XSAM marked this conversation as resolved.
ctx := prop.Extract(t.Context(), propagation.HeaderCarrier(req.Header))
got := baggage.FromContext(ctx)
assert.Equal(t, 0, got.Len(), "oversized header should result in empty baggage")
}

func TestExtractManyBaggageHeadersAggregateBudget(t *testing.T) {
prop := propagation.Baggage{}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody)

// Send 100 baggage headers, each ~200 bytes. Total: ~20KB.
// Only headers fitting within the 8192-byte aggregate budget should be processed.
for i := range 100 {
req.Header.Add("baggage", fmt.Sprintf("k%d=%s", i, strings.Repeat("v", 190)))
}

ctx := prop.Extract(t.Context(), propagation.HeaderCarrier(req.Header))
got := baggage.FromContext(ctx)

// Each header is ~195 bytes. Budget of 8192 / ~195 = ~42 headers processed.
// The exact count depends on key length (k0..k99), but should be well under 100.
assert.Less(t, got.Len(), 100, "should not process all 100 headers")
assert.Positive(t, got.Len(), "should process some headers")
}
Loading