Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 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
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
- `go.opentelemetry.io/otel/sdk/log` now unwraps error chains created with `fmt.Errorf` when deriving the `error.type` attribute from errors on log records. (#8133)
- `Set.MarshalLog` method in `go.opentelemetry.io/otel/attribute` now uses `Value.String` formatting following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8169)
- Optimize `go.opentelemetry.io/otel/sdk/metric` to return a drop reservoir when `exemplar.AlwaysOffFilter` is configured. (#8211)
- ⚠️ **Breaking Change:** Enforce the 8192-byte baggage size limit during extraction/parsing, changing behavior when the limit is exceeded in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`. (#8222)
Comment thread
XSAM marked this conversation as resolved.
Outdated

### Deprecated

Expand Down
22 changes: 18 additions & 4 deletions 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 @@ -493,9 +494,10 @@ func New(members ...Member) (Baggage, error) {
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
//
// If the baggage-string exceeds the maximum allowed members (64) or bytes
// (8192), members are dropped until the limits are satisfied and an error is
// returned along with the partial result.
// If the baggage-string exceeds the maximum allowed bytes (8192), an empty
// Baggage and an error are returned. If the baggage-string exceeds the maximum
// allowed members (64), members are dropped until the limit is satisfied and
// an error is returned along with the partial result.
Comment thread
XSAM marked this conversation as resolved.
Outdated
//
// Invalid members are skipped and the error is returned along with the
// partial result containing the valid members.
Expand All @@ -504,9 +506,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 +524,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 +563,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 member(s)", dropped))
}

if len(b) == 0 {
return Baggage{}, truncateErr
}
Expand Down
78 changes: 63 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,57 @@ 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 member(s)")
}

func TestParseErrorCapAllInvalid(t *testing.T) {
// All members invalid, no valid members. Exercises the len(b)==0
// return path with a capped error message.
var parts []string
for i := range 20 {
parts = append(parts, fmt.Sprintf("bad%d", i))
}
bStr := strings.Join(parts, ",")

b, err := Parse(bStr)
assert.ErrorIs(t, err, errInvalidMember)
assert.Equal(t, 0, b.Len(), "should return empty baggage")

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 member(s)")
}
44 changes: 41 additions & 3 deletions propagation/baggage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package propagation // import "go.opentelemetry.io/otel/propagation"

import (
"context"
"errors"
"fmt"

"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/internal/errorhandler"
Expand All @@ -15,7 +17,9 @@ const (

// W3C Baggage specification limits.
// https://www.w3.org/TR/baggage/#limits
maxMembers = 64
maxMembers = 64
maxBytesPerBaggageString = 8192
maxParseErrors = 5
Comment thread
XSAM marked this conversation as resolved.
Outdated
)

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

var members []baggage.Member
var totalBytes int
var parseErrors int
var truncateErr error
for _, bStr := range bVals {
Comment thread
XSAM marked this conversation as resolved.
Outdated
totalBytes += len(bStr)
Comment thread
pellared marked this conversation as resolved.
if totalBytes > maxBytesPerBaggageString {
parseErrors++
if parseErrors <= maxParseErrors {
Comment on lines +88 to +90
truncateErr = errors.Join(
truncateErr,
fmt.Errorf(
"baggage: aggregate header size %d exceeds %d byte limit",
totalBytes,
maxBytesPerBaggageString,
),
)
}
break
}
Comment thread
XSAM marked this conversation as resolved.

currBag, err := baggage.Parse(bStr)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
if uw, ok := err.(interface{ Unwrap() []error }); ok {
parseErrors += len(uw.Unwrap())
} else {
parseErrors++
}
Comment thread
XSAM marked this conversation as resolved.
if parseErrors <= maxParseErrors {
truncateErr = errors.Join(truncateErr, err)
}
Comment thread
XSAM marked this conversation as resolved.
Comment on lines +105 to +112
Copy link
Copy Markdown
Member

@pellared pellared May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one header value returns a capped joined error from baggage.Parse, extractMultiBaggage counts len(Unwrap()) and may drop that error entirely once it crosses maxParseErrors, then only emits and 1 more error(s).

The existing propagation test covers many invalid headers, not one header with many invalid members.

}
if currBag.Len() == 0 {
continue
Expand All @@ -86,10 +116,18 @@ func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.C
}
}

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

b, err := baggage.New(members...)
if err != nil {
errorhandler.GetErrorHandler().Handle(err)
truncateErr = errors.Join(truncateErr, err)
Comment thread
pellared marked this conversation as resolved.
}
if truncateErr != nil {
errorhandler.GetErrorHandler().Handle(truncateErr)
}

if b.Len() == 0 {
return parent
}
Expand Down
95 changes: 92 additions & 3 deletions propagation/baggage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/propagation"
)

const maxBytesPerBaggageString = 8192

type property struct {
Key, Value string
}
Expand Down Expand Up @@ -164,7 +167,6 @@ func generateMembers(n int, prefix string) members {
func TestExtractValidMultipleBaggageHeaders(t *testing.T) {
// W3C Baggage spec limits: https://www.w3.org/TR/baggage/#limits
const maxMembers = 64
const maxBytesPerBaggageString = 8192

prop := propagation.TextMapPropagator(propagation.Baggage{})
tests := []struct {
Expand Down Expand Up @@ -297,7 +299,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 +308,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 +499,91 @@ 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", maxBytesPerBaggageString))

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")
}

type errHandler struct {
err error
}

func (e *errHandler) Handle(err error) { e.err = err }

func TestExtractManyBaggageHeader(t *testing.T) {
tests := []struct {
name string
headers func() []string
want func() members
wantErrStr []string
}{
{
name: "aggregate byte budget exceeded",
headers: func() []string {
// 100 headers, each ~195 bytes. Total: ~19.5KB, well over 8192.
h := make([]string, 100)
for i := range 100 {
h[i] = fmt.Sprintf("k%d=%s", i, strings.Repeat("v", 190))
}
return h
},
want: func() members {
m := make(members, 42)
for i := range 42 {
m[i] = member{Key: fmt.Sprintf("k%d", i), Value: strings.Repeat("v", 190)}
}
return m
},
wantErrStr: []string{"aggregate header size 8332 exceeds 8192 byte limit"},
},
{
name: "too many invalid headers triggers error cap",
headers: func() []string {
// 10 invalid headers (no '=' delimiter) followed by 1 valid.
// Each invalid header produces 1 parse error from baggage.Parse.
// maxParseErrors=5, so 5 errors are kept and 5 are summarized.
h := make([]string, 11)
for i := range 10 {
h[i] = fmt.Sprintf("bad%d", i)
}
h[10] = "good=val"
return h
},
want: func() members {
return members{{Key: "good", Value: "val"}}
},
wantErrStr: []string{"invalid baggage list-member", "and 5 more error(s)"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalErrorHandler := otel.GetErrorHandler()
eh := &errHandler{}
otel.SetErrorHandler(eh)
t.Cleanup(func() { otel.SetErrorHandler(originalErrorHandler) })

prop := propagation.Baggage{}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody)
for _, h := range tt.headers() {
req.Header.Add("baggage", h)
}

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

assert.Equal(t, tt.want().Baggage(t), got)
assert.Error(t, eh.err)
for _, s := range tt.wantErrStr {
assert.Contains(t, eh.err.Error(), s)
}
})
}
}
Loading