Skip to content

Commit 03837d3

Browse files
authored
Apply baggage limits (#8380)
1 parent cdadad6 commit 03837d3

7 files changed

Lines changed: 427 additions & 21 deletions

File tree

api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Parser.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private enum State {
2727
}
2828

2929
private final String baggageHeader;
30+
private final int maxEntries;
3031

3132
private final Element key = Element.createKeyElement();
3233
private final Element value = Element.createValueElement();
@@ -36,14 +37,19 @@ private enum State {
3637
private int metaStart;
3738

3839
private boolean skipToNext;
40+
private int entriesAdded;
3941

40-
Parser(String baggageHeader) {
42+
Parser(String baggageHeader, int maxEntries) {
4143
this.baggageHeader = baggageHeader;
44+
this.maxEntries = maxEntries;
4245
reset(0);
4346
}
4447

45-
void parseInto(BaggageBuilder baggageBuilder) {
48+
int parseInto(BaggageBuilder baggageBuilder) {
4649
for (int i = 0, n = baggageHeader.length(); i < n; i++) {
50+
if (entriesAdded >= maxEntries) {
51+
break;
52+
}
4753
char current = baggageHeader.charAt(i);
4854

4955
if (skipToNext) {
@@ -123,13 +129,17 @@ void parseInto(BaggageBuilder baggageBuilder) {
123129
}
124130
}
125131
}
132+
return entriesAdded;
126133
}
127134

128-
private static void putBaggage(
135+
private void putBaggage(
129136
BaggageBuilder baggage,
130137
@Nullable String key,
131138
@Nullable String value,
132139
@Nullable String metadataValue) {
140+
if (entriesAdded >= maxEntries) {
141+
return;
142+
}
133143
String decodedValue = decodeValue(value);
134144
metadataValue = decodeValue(metadataValue);
135145
BaggageEntryMetadata baggageEntryMetadata =
@@ -138,6 +148,7 @@ private static void putBaggage(
138148
: BaggageEntryMetadata.empty();
139149
if (key != null && decodedValue != null) {
140150
baggage.put(key, decodedValue, baggageEntryMetadata);
151+
entriesAdded++;
141152
}
142153
}
143154

api/all/src/main/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagator.java

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@
1919
import java.util.Collection;
2020
import java.util.Iterator;
2121
import java.util.List;
22+
import java.util.logging.Logger;
2223
import javax.annotation.Nullable;
2324

2425
/**
2526
* {@link TextMapPropagator} that implements the W3C specification for baggage header propagation.
2627
*/
2728
public final class W3CBaggagePropagator implements TextMapPropagator {
2829

30+
// Limits from https://www.w3.org/TR/baggage/#limits
31+
private static final int MAX_BAGGAGE_ENTRIES = 64;
32+
private static final int MAX_BAGGAGE_BYTES = 8192;
33+
2934
private static final String FIELD = "baggage";
3035
private static final List<String> FIELDS = singletonList(FIELD);
3136
private static final W3CBaggagePropagator INSTANCE = new W3CBaggagePropagator();
3237
private static final PercentEscaper URL_ESCAPER = PercentEscaper.create();
38+
private static final Logger LOGGER = Logger.getLogger(W3CBaggagePropagator.class.getName());
3339

3440
/** Singleton instance of the W3C Baggage Propagator. */
3541
public static W3CBaggagePropagator getInstance() {
@@ -61,17 +67,34 @@ public <C> void inject(Context context, @Nullable C carrier, TextMapSetter<C> se
6167

6268
private static String baggageToString(Baggage baggage) {
6369
StringBuilder headerContent = new StringBuilder();
70+
int[] entryCount = {0};
6471
baggage.forEach(
6572
(key, baggageEntry) -> {
6673
if (baggageIsInvalid(key, baggageEntry)) {
6774
return;
6875
}
69-
headerContent.append(key).append("=").append(encodeValue(baggageEntry.getValue()));
76+
if (entryCount[0] >= MAX_BAGGAGE_ENTRIES) {
77+
return;
78+
}
79+
String encodedValue = encodeValue(baggageEntry.getValue());
7080
String metadataValue = baggageEntry.getMetadata().getValue();
71-
if (metadataValue != null && !metadataValue.isEmpty()) {
72-
headerContent.append(";").append(encodeValue(metadataValue));
81+
String encodedMetadata =
82+
(metadataValue != null && !metadataValue.isEmpty())
83+
? encodeValue(metadataValue)
84+
: null;
85+
// Exit early if adding this entry causes the total length to exceed the limit
86+
// encodedEntryLength includes a trailing comma; the final string trims exactly one,
87+
// so the net contribution to the final length is entryLength - 1.
88+
if (headerContent.length() + encodedEntryLength(key, encodedValue, encodedMetadata) - 1
89+
> MAX_BAGGAGE_BYTES) {
90+
return;
91+
}
92+
headerContent.append(key).append("=").append(encodedValue);
93+
if (encodedMetadata != null) {
94+
headerContent.append(";").append(encodedMetadata);
7395
}
7496
headerContent.append(",");
97+
entryCount[0]++;
7598
});
7699

77100
if (headerContent.length() == 0) {
@@ -87,6 +110,21 @@ private static String encodeValue(String value) {
87110
return URL_ESCAPER.escape(value);
88111
}
89112

113+
/**
114+
* Returns the length of the serialized entry as it would appear in the baggage header, including
115+
* the trailing comma used by the trailing-comma pattern in {@link #baggageToString}. The length
116+
* accounts for {@code "key=encodedValue,"} plus {@code ";encodedMetadata"} when metadata is
117+
* present.
118+
*/
119+
private static int encodedEntryLength(
120+
String key, String encodedValue, @Nullable String encodedMetadata) {
121+
int length = key.length() + 1 + encodedValue.length() + 1; // "key=value,"
122+
if (encodedMetadata != null) {
123+
length += 1 + encodedMetadata.length(); // ";metadata"
124+
}
125+
return length;
126+
}
127+
90128
@Override
91129
public <C> Context extract(Context context, @Nullable C carrier, TextMapGetter<C> getter) {
92130
if (context == null) {
@@ -108,16 +146,25 @@ private static <C> Context extractMulti(
108146

109147
boolean extracted = false;
110148
BaggageBuilder baggageBuilder = Baggage.builder();
149+
int totalBytes = 0;
150+
int totalEntries = 0;
111151

112152
while (baggageHeaders.hasNext()) {
113153
String header = baggageHeaders.next();
114154
if (header.isEmpty()) {
115155
continue;
116156
}
117157

158+
totalBytes += header.length();
159+
if (totalBytes > MAX_BAGGAGE_BYTES || totalEntries >= MAX_BAGGAGE_ENTRIES) {
160+
LOGGER.fine("Baggage header exceeded W3C limits, dropping remaining entries");
161+
break;
162+
}
163+
118164
try {
119-
extractEntries(header, baggageBuilder);
165+
int added = extractEntries(header, baggageBuilder, MAX_BAGGAGE_ENTRIES - totalEntries);
120166
extracted = true;
167+
totalEntries += added;
121168
} catch (RuntimeException expected) {
122169
// invalid baggage header, continue
123170
}
@@ -126,8 +173,9 @@ private static <C> Context extractMulti(
126173
return extracted ? context.with(baggageBuilder.build()) : context;
127174
}
128175

129-
private static void extractEntries(String baggageHeader, BaggageBuilder baggageBuilder) {
130-
new Parser(baggageHeader).parseInto(baggageBuilder);
176+
private static int extractEntries(
177+
String baggageHeader, BaggageBuilder baggageBuilder, int maxEntries) {
178+
return new Parser(baggageHeader, maxEntries).parseInto(baggageBuilder);
131179
}
132180

133181
private static boolean baggageIsInvalid(String key, BaggageEntry baggageEntry) {

api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@
1111
import com.google.common.collect.ImmutableList;
1212
import com.google.common.collect.ImmutableMap;
1313
import io.opentelemetry.api.baggage.Baggage;
14+
import io.opentelemetry.api.baggage.BaggageBuilder;
1415
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
1516
import io.opentelemetry.context.Context;
1617
import io.opentelemetry.context.propagation.TextMapGetter;
18+
import java.util.Arrays;
1719
import java.util.Collections;
1820
import java.util.HashMap;
1921
import java.util.Iterator;
2022
import java.util.LinkedHashMap;
2123
import java.util.List;
2224
import java.util.Map;
25+
import java.util.stream.Stream;
2326
import javax.annotation.Nullable;
2427
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.Arguments;
30+
import org.junit.jupiter.params.provider.MethodSource;
2531

2632
class W3CBaggagePropagatorTest {
2733

@@ -595,6 +601,114 @@ void inject_nullSetter() {
595601
assertThat(carrier).isEmpty();
596602
}
597603

604+
@ParameterizedTest
605+
@MethodSource
606+
void extract_limit_maxEntries(List<String> headers, Baggage expectedBaggage) {
607+
Context result =
608+
W3CBaggagePropagator.getInstance()
609+
.extract(Context.root(), ImmutableMap.of("baggage", headers), multiGetter);
610+
assertThat(Baggage.fromContext(result)).isEqualTo(expectedBaggage);
611+
}
612+
613+
static Stream<Arguments> extract_limit_maxEntries() {
614+
return Stream.of(
615+
// Exactly at the limit — all 64 entries extracted
616+
Arguments.of(ImmutableList.of(baggageHeader(0, 64)), baggageWithEntries(0, 64)),
617+
// One over the limit — only the first 64 extracted
618+
Arguments.of(ImmutableList.of(baggageHeader(0, 65)), baggageWithEntries(0, 64)),
619+
// Split across two headers — only the first 64 total extracted
620+
Arguments.of(
621+
ImmutableList.of(baggageHeader(0, 32), baggageHeader(32, 33)),
622+
baggageWithEntries(0, 64)));
623+
}
624+
625+
/**
626+
* Builds a {@link Baggage} with entries {@code k{start}=v{start}} through {@code
627+
* k{start+count-1}=v{start+count-1}}.
628+
*/
629+
private static Baggage baggageWithEntries(int start, int count) {
630+
BaggageBuilder builder = Baggage.builder();
631+
for (int i = start; i < start + count; i++) {
632+
builder.put("k" + i, "v" + i);
633+
}
634+
return builder.build();
635+
}
636+
637+
/** Builds {@code "k{start}=v{start},...,k{start+count-1}=v{start+count-1}"}. */
638+
private static String baggageHeader(int start, int count) {
639+
StringBuilder sb = new StringBuilder();
640+
for (int i = start; i < start + count; i++) {
641+
if (i > start) {
642+
sb.append(",");
643+
}
644+
sb.append("k").append(i).append("=v").append(i);
645+
}
646+
return sb.toString();
647+
}
648+
649+
@Test
650+
void extract_limit_maxBytes_exceedsLimit() {
651+
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
652+
// Single header over 8192 bytes — dropped entirely; partial values must not be extracted
653+
String header = "k=" + fillChars('v', 8192); // 8194 bytes
654+
Context result = propagator.extract(Context.root(), ImmutableMap.of("baggage", header), getter);
655+
assertThat(Baggage.fromContext(result)).isEqualTo(Baggage.empty());
656+
}
657+
658+
@Test
659+
void extract_limit_maxBytes_acrossMultipleHeaders() {
660+
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
661+
// First header just under 8192 bytes is extracted; second header pushes total over the limit
662+
String almostMax = "k=" + fillChars('v', 8189); // "k=vvv..."
663+
String second = "k2=v2";
664+
Context result =
665+
propagator.extract(
666+
Context.root(),
667+
ImmutableMap.of("baggage", ImmutableList.of(almostMax, second)),
668+
multiGetter);
669+
// Only the first header should have been extracted
670+
assertThat(Baggage.fromContext(result).size()).isEqualTo(1);
671+
assertThat(Baggage.fromContext(result).getEntryValue("k2")).isNull();
672+
}
673+
674+
@Test
675+
void inject_limit_maxEntries() {
676+
Map<String, String> carrier = new HashMap<>();
677+
W3CBaggagePropagator.getInstance()
678+
.inject(Context.root().with(baggageWithEntries(0, 74)), carrier, Map::put);
679+
String header = carrier.get("baggage");
680+
assertThat(header).isNotNull();
681+
long count = header.chars().filter(c -> c == '=').count();
682+
assertThat(count).isEqualTo(64);
683+
}
684+
685+
@Test
686+
void inject_limit_maxBytes() {
687+
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
688+
// One entry whose encoded form alone exceeds the byte limit — should produce empty header
689+
Baggage baggage = Baggage.builder().put("k", fillChars('v', 8192)).build();
690+
Map<String, String> carrier = new HashMap<>();
691+
propagator.inject(Context.root().with(baggage), carrier, Map::put);
692+
assertThat(carrier).doesNotContainKey("baggage");
693+
}
694+
695+
@Test
696+
void inject_limit_maxBytes_metadata() {
697+
// Value alone fits easily (k=v is 3 bytes), but k=v;{metadata} exceeds 8192 bytes.
698+
// Verifies that metadata length is included in the byte limit check.
699+
Baggage baggage =
700+
Baggage.builder().put("k", "v", BaggageEntryMetadata.create(fillChars('x', 8190))).build();
701+
Map<String, String> carrier = new HashMap<>();
702+
W3CBaggagePropagator.getInstance().inject(Context.root().with(baggage), carrier, Map::put);
703+
assertThat(carrier).doesNotContainKey("baggage");
704+
}
705+
706+
private static String fillChars(char c, int count) {
707+
char[] chars = new char[count];
708+
Arrays.fill(chars, c);
709+
return new String(chars);
710+
}
711+
598712
@Test
599713
void toString_Valid() {
600714
assertThat(W3CBaggagePropagator.getInstance().toString()).isEqualTo("W3CBaggagePropagator");

0 commit comments

Comments
 (0)