From 788232e1d78cc149c1a3ece99ebd7b3738e3f380 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 29 Oct 2026 07:27:35 -0300 Subject: [PATCH] jooby-opentelemetry: repeated request header causes 400 (TypeMismatchException in DefaultOtelContextExtractor) fixes #4036 --- .../java/io/jooby/internal/HeadersValue.java | 2 +- .../DefaultOtelContextExtractor.java | 10 ++- .../DefaultOtelContextExtractorTest.java | 5 +- ...efaultOtelContextHeadersExtractorTest.java | 86 +++++++++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextHeadersExtractorTest.java diff --git a/jooby/src/main/java/io/jooby/internal/HeadersValue.java b/jooby/src/main/java/io/jooby/internal/HeadersValue.java index dcfbc2f4e5..4bad5e2ebb 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadersValue.java +++ b/jooby/src/main/java/io/jooby/internal/HeadersValue.java @@ -30,7 +30,7 @@ protected Map hash() { @Override public Map toMap() { Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - toMultimap().forEach((k, v) -> map.put(k, v.get(0))); + toMultimap().forEach((k, v) -> map.put(k, v.getFirst())); return map; } diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractor.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractor.java index e6968a82db..52ea5a3d4c 100644 --- a/modules/jooby-opentelemetry/src/main/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractor.java +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractor.java @@ -7,6 +7,8 @@ import static io.opentelemetry.context.Context.current; +import java.util.Iterator; + import org.jspecify.annotations.NonNull; import io.jooby.opentelemetry.OtelContextExtractor; @@ -52,8 +54,14 @@ public Iterable keys(io.jooby.Context ctx) { @Override public String get(io.jooby.Context ctx, String key) { + var value = ctx.header(key).toList(); // Safely extract the header value, returning null if it doesn't exist - return ctx.header(key).valueOrNull(); + return value.isEmpty() ? null : value.getFirst(); + } + + @Override + public Iterator getAll(io.jooby.Context ctx, String key) { + return ctx.header(key).toList().iterator(); } } } diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractorTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractorTest.java index 32b1aee19e..8efa57309e 100644 --- a/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractorTest.java +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextExtractorTest.java @@ -14,6 +14,7 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.util.List; import java.util.Map; import java.util.Set; @@ -108,8 +109,8 @@ void joobyRequestGetterShouldReturnHeaderKeys() { void joobyRequestGetterShouldReturnHeaderValueOrNull() { // Arrange Value mockHeaderValue = mock(Value.class); - when(mockHeaderValue.valueOrNull()) - .thenReturn("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + when(mockHeaderValue.toList()) + .thenReturn(List.of("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); when(joobyCtx.header("traceparent")).thenReturn(mockHeaderValue); // Act diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextHeadersExtractorTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextHeadersExtractorTest.java new file mode 100644 index 0000000000..3a33573b0b --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/internal/opentelemetry/DefaultOtelContextHeadersExtractorTest.java @@ -0,0 +1,86 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.opentelemetry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.jooby.Context; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; + +class DefaultOtelContextHeadersExtractorTest { + + @Test + void shouldExtractKeys() { + var ctx = mock(io.jooby.Context.class); + when(ctx.headerMap()) + .thenReturn( + Map.of("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + headers.keys(ctx); + } + + @Test + void shouldExtractHeaderValue() { + var ctx = mock(io.jooby.Context.class); + var headerValue = mock(io.jooby.value.Value.class); + when(headerValue.toList()) + .thenReturn(java.util.List.of("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); + when(ctx.header("traceparent")).thenReturn(headerValue); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + headers.get(ctx, "traceparent"); + } + + @Test + void shouldExtractHeaderRealSingleValue() { + var ctx = mock(io.jooby.Context.class); + var firstValue = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; + var headerValue = Value.value(new ValueFactory(), "traceparent", firstValue); + when(ctx.header("traceparent")).thenReturn(headerValue); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + assertEquals(firstValue, headers.get(ctx, "traceparent")); + } + + @Test + void shouldExtractHeaderRealListValue() { + var ctx = mock(io.jooby.Context.class); + var firstValue = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"; + var secondValue = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-02"; + var headerValue = + Value.array(new ValueFactory(), "traceparent", List.of(firstValue, secondValue)); + when(ctx.header("traceparent")).thenReturn(headerValue); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + assertEquals(firstValue, headers.get(ctx, "traceparent")); + } + + @Test + void shouldExtractHeaderRealEmptyValue() { + var ctx = mock(io.jooby.Context.class); + var headerValue = Value.missing(new ValueFactory(), "traceparent"); + when(ctx.header("traceparent")).thenReturn(headerValue); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + assertEquals(null, headers.get(ctx, "traceparent")); + } + + @Test + void shouldExtractHeaderAllValue() { + var ctx = mock(Context.class); + var headerValue = mock(Value.class); + var values = List.of("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + when(headerValue.toList()).thenReturn(values); + when(ctx.header("traceparent")).thenReturn(headerValue); + var headers = DefaultOtelContextExtractor.Headers.INSTANCE; + assertThat(headers.getAll(ctx, "traceparent")).toIterable().containsExactlyElementsOf(values); + } +}