Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ private enum State {
}

private final String baggageHeader;
private final int maxEntries;

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

private boolean skipToNext;
private int entriesAdded;

Parser(String baggageHeader) {
Parser(String baggageHeader, int maxEntries) {
this.baggageHeader = baggageHeader;
this.maxEntries = maxEntries;
reset(0);
}

void parseInto(BaggageBuilder baggageBuilder) {
int parseInto(BaggageBuilder baggageBuilder) {
for (int i = 0, n = baggageHeader.length(); i < n; i++) {
if (entriesAdded >= maxEntries) {
break;
}
char current = baggageHeader.charAt(i);

if (skipToNext) {
Expand Down Expand Up @@ -123,13 +129,17 @@ void parseInto(BaggageBuilder baggageBuilder) {
}
}
}
return entriesAdded;
}

private static void putBaggage(
private void putBaggage(
BaggageBuilder baggage,
@Nullable String key,
@Nullable String value,
@Nullable String metadataValue) {
if (entriesAdded >= maxEntries) {
return;
}
String decodedValue = decodeValue(value);
metadataValue = decodeValue(metadataValue);
BaggageEntryMetadata baggageEntryMetadata =
Expand All @@ -138,6 +148,7 @@ private static void putBaggage(
: BaggageEntryMetadata.empty();
if (key != null && decodedValue != null) {
baggage.put(key, decodedValue, baggageEntryMetadata);
entriesAdded++;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;

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

// Limits from https://www.w3.org/TR/baggage/#limits
private static final int MAX_BAGGAGE_ENTRIES = 64;
private static final int MAX_BAGGAGE_BYTES = 8192;

private static final String FIELD = "baggage";
private static final List<String> FIELDS = singletonList(FIELD);
private static final W3CBaggagePropagator INSTANCE = new W3CBaggagePropagator();
private static final PercentEscaper URL_ESCAPER = PercentEscaper.create();
private static final Logger LOGGER = Logger.getLogger(W3CBaggagePropagator.class.getName());

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

private static String baggageToString(Baggage baggage) {
StringBuilder headerContent = new StringBuilder();
int[] entryCount = {0};
baggage.forEach(
(key, baggageEntry) -> {
if (baggageIsInvalid(key, baggageEntry)) {
return;
}
headerContent.append(key).append("=").append(encodeValue(baggageEntry.getValue()));
if (entryCount[0] >= MAX_BAGGAGE_ENTRIES) {
return;
}
String encodedValue = encodeValue(baggageEntry.getValue());
String metadataValue = baggageEntry.getMetadata().getValue();
if (metadataValue != null && !metadataValue.isEmpty()) {
headerContent.append(";").append(encodeValue(metadataValue));
String encodedMetadata =
(metadataValue != null && !metadataValue.isEmpty())
? encodeValue(metadataValue)
: null;
// Exit early if adding this entry causes the total length to exceed the limit
// encodedEntryLength includes a trailing comma; the final string trims exactly one,
// so the net contribution to the final length is entryLength - 1.
if (headerContent.length() + encodedEntryLength(key, encodedValue, encodedMetadata) - 1
> MAX_BAGGAGE_BYTES) {
return;
}
headerContent.append(key).append("=").append(encodedValue);
if (encodedMetadata != null) {
headerContent.append(";").append(encodedMetadata);
}
headerContent.append(",");
entryCount[0]++;
});

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

/**
* Returns the length of the serialized entry as it would appear in the baggage header, including
* the trailing comma used by the trailing-comma pattern in {@link #baggageToString}. The length
* accounts for {@code "key=encodedValue,"} plus {@code ";encodedMetadata"} when metadata is
* present.
*/
private static int encodedEntryLength(
String key, String encodedValue, @Nullable String encodedMetadata) {
int length = key.length() + 1 + encodedValue.length() + 1; // "key=value,"
if (encodedMetadata != null) {
length += 1 + encodedMetadata.length(); // ";metadata"
}
return length;
}

@Override
public <C> Context extract(Context context, @Nullable C carrier, TextMapGetter<C> getter) {
if (context == null) {
Expand All @@ -108,16 +146,25 @@ private static <C> Context extractMulti(

boolean extracted = false;
BaggageBuilder baggageBuilder = Baggage.builder();
int totalBytes = 0;
int totalEntries = 0;

while (baggageHeaders.hasNext()) {
String header = baggageHeaders.next();
if (header.isEmpty()) {
continue;
}

totalBytes += header.length();
if (totalBytes > MAX_BAGGAGE_BYTES || totalEntries >= MAX_BAGGAGE_ENTRIES) {
LOGGER.fine("Baggage header exceeded W3C limits, dropping remaining entries");
break;
}

try {
extractEntries(header, baggageBuilder);
int added = extractEntries(header, baggageBuilder, MAX_BAGGAGE_ENTRIES - totalEntries);
extracted = true;
totalEntries += added;
} catch (RuntimeException expected) {
// invalid baggage header, continue
}
Expand All @@ -126,8 +173,9 @@ private static <C> Context extractMulti(
return extracted ? context.with(baggageBuilder.build()) : context;
}

private static void extractEntries(String baggageHeader, BaggageBuilder baggageBuilder) {
new Parser(baggageHeader).parseInto(baggageBuilder);
private static int extractEntries(
String baggageHeader, BaggageBuilder baggageBuilder, int maxEntries) {
return new Parser(baggageHeader, maxEntries).parseInto(baggageBuilder);
}

private static boolean baggageIsInvalid(String key, BaggageEntry baggageEntry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.BaggageBuilder;
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class W3CBaggagePropagatorTest {

Expand Down Expand Up @@ -595,6 +601,114 @@ void inject_nullSetter() {
assertThat(carrier).isEmpty();
}

@ParameterizedTest
@MethodSource
void extract_limit_maxEntries(List<String> headers, Baggage expectedBaggage) {
Context result =
W3CBaggagePropagator.getInstance()
.extract(Context.root(), ImmutableMap.of("baggage", headers), multiGetter);
assertThat(Baggage.fromContext(result)).isEqualTo(expectedBaggage);
}

static Stream<Arguments> extract_limit_maxEntries() {
return Stream.of(
// Exactly at the limit — all 64 entries extracted
Arguments.of(ImmutableList.of(baggageHeader(0, 64)), baggageWithEntries(0, 64)),
// One over the limit — only the first 64 extracted
Arguments.of(ImmutableList.of(baggageHeader(0, 65)), baggageWithEntries(0, 64)),
// Split across two headers — only the first 64 total extracted
Arguments.of(
ImmutableList.of(baggageHeader(0, 32), baggageHeader(32, 33)),
baggageWithEntries(0, 64)));
}

/**
* Builds a {@link Baggage} with entries {@code k{start}=v{start}} through {@code
* k{start+count-1}=v{start+count-1}}.
*/
private static Baggage baggageWithEntries(int start, int count) {
BaggageBuilder builder = Baggage.builder();
for (int i = start; i < start + count; i++) {
builder.put("k" + i, "v" + i);
}
return builder.build();
}

/** Builds {@code "k{start}=v{start},...,k{start+count-1}=v{start+count-1}"}. */
private static String baggageHeader(int start, int count) {
StringBuilder sb = new StringBuilder();
for (int i = start; i < start + count; i++) {
if (i > start) {
sb.append(",");
}
sb.append("k").append(i).append("=v").append(i);
}
return sb.toString();
}

@Test
void extract_limit_maxBytes_exceedsLimit() {
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
// Single header over 8192 bytes — dropped entirely; partial values must not be extracted
String header = "k=" + fillChars('v', 8192); // 8194 bytes
Context result = propagator.extract(Context.root(), ImmutableMap.of("baggage", header), getter);
assertThat(Baggage.fromContext(result)).isEqualTo(Baggage.empty());
}

@Test
void extract_limit_maxBytes_acrossMultipleHeaders() {
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
// First header just under 8192 bytes is extracted; second header pushes total over the limit
String almostMax = "k=" + fillChars('v', 8189); // "k=vvv..."
String second = "k2=v2";
Context result =
propagator.extract(
Context.root(),
ImmutableMap.of("baggage", ImmutableList.of(almostMax, second)),
multiGetter);
// Only the first header should have been extracted
assertThat(Baggage.fromContext(result).size()).isEqualTo(1);
assertThat(Baggage.fromContext(result).getEntryValue("k2")).isNull();
}

@Test
void inject_limit_maxEntries() {
Map<String, String> carrier = new HashMap<>();
W3CBaggagePropagator.getInstance()
.inject(Context.root().with(baggageWithEntries(0, 74)), carrier, Map::put);
String header = carrier.get("baggage");
assertThat(header).isNotNull();
long count = header.chars().filter(c -> c == '=').count();
assertThat(count).isEqualTo(64);
}

@Test
void inject_limit_maxBytes() {
W3CBaggagePropagator propagator = W3CBaggagePropagator.getInstance();
// One entry whose encoded form alone exceeds the byte limit — should produce empty header
Baggage baggage = Baggage.builder().put("k", fillChars('v', 8192)).build();
Map<String, String> carrier = new HashMap<>();
propagator.inject(Context.root().with(baggage), carrier, Map::put);
assertThat(carrier).doesNotContainKey("baggage");
}

@Test
void inject_limit_maxBytes_metadata() {
// Value alone fits easily (k=v is 3 bytes), but k=v;{metadata} exceeds 8192 bytes.
// Verifies that metadata length is included in the byte limit check.
Baggage baggage =
Baggage.builder().put("k", "v", BaggageEntryMetadata.create(fillChars('x', 8190))).build();
Map<String, String> carrier = new HashMap<>();
W3CBaggagePropagator.getInstance().inject(Context.root().with(baggage), carrier, Map::put);
assertThat(carrier).doesNotContainKey("baggage");
}

private static String fillChars(char c, int count) {
char[] chars = new char[count];
Arrays.fill(chars, c);
return new String(chars);
}

@Test
void toString_Valid() {
assertThat(W3CBaggagePropagator.getInstance().toString()).isEqualTo("W3CBaggagePropagator");
Expand Down
Loading
Loading