From b5ea38fd4733557bcc361c32e4f5b87b6f3c2a20 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Wed, 19 Nov 2025 13:29:45 -0400 Subject: [PATCH 1/8] Use case-insensitive comparision when extracting headers Run tests ./gradlew :sentry:test --tests="*NetworkDetailCaptureUtilsTest*" --- .../network/NetworkDetailCaptureUtils.java | 24 ++++-- .../network/NetworkDetailCaptureUtilsTest.kt | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt diff --git a/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java b/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java index 3ed8e1e1674..21fcf2d0a35 100644 --- a/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java +++ b/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java @@ -1,9 +1,12 @@ package io.sentry.util.network; -import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; /** * Utility class for network capture operations shared across HTTP client integrations. Provides @@ -115,19 +118,24 @@ private static boolean shouldCaptureUrl( return false; } - private static @NotNull Map getCaptureHeaders( + @VisibleForTesting + static @NotNull Map getCaptureHeaders( @Nullable final Map allHeaders, @NotNull final String[] allowedHeaders) { - Map capturedHeaders = new HashMap<>(); - - if (allHeaders == null) { + final Map capturedHeaders = new LinkedHashMap<>(); + if (allHeaders == null || allowedHeaders.length == 0) { return capturedHeaders; } + // Convert to lowercase for case-insensitive matching + Set normalizedAllowed = new HashSet<>(); for (String header : allowedHeaders) { - String value = allHeaders.get(header); - if (value != null) { - capturedHeaders.put(header, value); + normalizedAllowed.add(header.toLowerCase()); + } + + for (Map.Entry entry : allHeaders.entrySet()) { + if (normalizedAllowed.contains(entry.getKey().toLowerCase())) { + capturedHeaders.put(entry.getKey(), entry.getValue()); } } diff --git a/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt b/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt new file mode 100644 index 00000000000..94a5398acd5 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt @@ -0,0 +1,81 @@ +package io.sentry.util.network + +import java.util.LinkedHashMap +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.Test + +class NetworkDetailCaptureUtilsTest { + + @Test + fun `getCaptureHeaders should match headers case-insensitively`() { + // Setup: allHeaders with mixed case keys + val allHeaders = + LinkedHashMap().apply { + put("Content-Type", "application/json") + put("Authorization", "Bearer token123") + put("X-Custom-Header", "custom-value") + put("accept", "application/json") + } + + // Test: allowedHeaders with different casing + val allowedHeaders = arrayOf("content-type", "AUTHORIZATION", "x-custom-header", "ACCEPT") + + val result = NetworkDetailCaptureUtils.getCaptureHeaders(allHeaders, allowedHeaders) + + // All headers should be matched despite case differences + assertEquals(4, result.size) + + // Original casing should be preserved in output + assertEquals("application/json", result["Content-Type"]) + assertEquals("Bearer token123", result["Authorization"]) + assertEquals("custom-value", result["X-Custom-Header"]) + assertEquals("application/json", result["accept"]) + + // Verify keys maintain original casing from allHeaders + assertTrue(result.containsKey("Content-Type")) + assertTrue(result.containsKey("Authorization")) + assertTrue(result.containsKey("X-Custom-Header")) + assertTrue(result.containsKey("accept")) + } + + @Test + fun `getCaptureHeaders should handle null allHeaders`() { + val allowedHeaders = arrayOf("content-type") + + val result = NetworkDetailCaptureUtils.getCaptureHeaders(null, allowedHeaders) + + assertTrue(result.isEmpty()) + } + + @Test + fun `getCaptureHeaders should handle empty allowedHeaders`() { + val allHeaders = mapOf("Content-Type" to "application/json") + val allowedHeaders = arrayOf() + + val result = NetworkDetailCaptureUtils.getCaptureHeaders(allHeaders, allowedHeaders) + + assertTrue(result.isEmpty()) + } + + @Test + fun `getCaptureHeaders should only capture allowed headers`() { + val allHeaders = + mapOf( + "Content-Type" to "application/json", + "Authorization" to "Bearer token123", + "X-Unwanted-Header" to "should-not-appear", + ) + + val allowedHeaders = arrayOf("content-type", "authorization") + + val result = NetworkDetailCaptureUtils.getCaptureHeaders(allHeaders, allowedHeaders) + + assertEquals(2, result.size) + assertEquals("application/json", result["Content-Type"]) + assertEquals("Bearer token123", result["Authorization"]) + + // Unwanted header should not be present + assertTrue(!result.containsKey("X-Unwanted-Header")) + } +} From 584154434bd51581713514ef5e795e1c6a2c7aa8 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Wed, 19 Nov 2025 14:29:17 -0400 Subject: [PATCH 2/8] Extract network details when using SentryOkHttpEventListener Reuse existing logic that retrieves optional SentryOkHttpEvent for the okhttp3.Call, and optionally provide NetworkRequestData for adding to Breadcrumb Hint in SentryOkHttpEvent#finish --- .../main/java/io/sentry/okhttp/SentryOkHttpEvent.kt | 10 ++++++++++ .../java/io/sentry/okhttp/SentryOkHttpInterceptor.kt | 3 +++ .../samples/android/TriggerHttpRequestActivity.java | 7 ++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index fc894ec3768..7475f09443b 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -10,6 +10,7 @@ import io.sentry.TypeCheckHint import io.sentry.transport.CurrentDateProvider import io.sentry.util.Platform import io.sentry.util.UrlUtils +import io.sentry.util.network.NetworkRequestData import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -27,6 +28,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques internal val callSpan: ISpan? private var response: Response? = null private var clientErrorResponse: Response? = null + private var networkDetails: NetworkRequestData? = null internal val isEventFinished = AtomicBoolean(false) private var url: String private var method: String @@ -135,6 +137,11 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques } } + /** Sets the [NetworkRequestData] for network detail capture. */ + fun setNetworkDetails(networkRequestData: NetworkRequestData?) { + this.networkDetails = networkRequestData + } + /** Record event start if the callRootSpan is not null. */ fun onEventStart(event: String) { callSpan ?: return @@ -163,6 +170,9 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques hint.set(TypeCheckHint.OKHTTP_REQUEST, request) response?.let { hint.set(TypeCheckHint.OKHTTP_RESPONSE, it) } + // Include network details in the hint for session replay + networkDetails?.let { hint.set(TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS, it) } + // needs this as unix timestamp for rrweb breadcrumb.setData( SpanDataConvention.HTTP_END_TIMESTAMP, diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index bfcc5a033d4..5fa839ca891 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -209,6 +209,9 @@ public open class SentryOkHttpInterceptor( ) } + // Set network details on the OkHttpEvent so it can include them in the breadcrumb hint + okHttpEvent?.setNetworkDetails(networkDetailData) + finishSpan(span, request, response, isFromEventListener, okHttpEvent) // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/TriggerHttpRequestActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/TriggerHttpRequestActivity.java index 486b558a717..5671a044437 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/TriggerHttpRequestActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/TriggerHttpRequestActivity.java @@ -10,6 +10,7 @@ import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import io.sentry.Sentry; +import io.sentry.okhttp.SentryOkHttpEventListener; import io.sentry.okhttp.SentryOkHttpInterceptor; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -80,14 +81,14 @@ private void initializeViews() { private void setupOkHttpClient() { // OkHttpClient with Sentry integration for monitoring HTTP requests + // Both SentryOkHttpEventListener and SentryOkHttpInterceptor are enabled to test + // network detail capture when both components are used together okHttpClient = new OkHttpClient.Builder() .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) - // performance monitoring - // .eventListener(new SentryOkHttpEventListener()) - // breadcrumbs and failed request capture + .eventListener(new SentryOkHttpEventListener()) .addInterceptor(new SentryOkHttpInterceptor()) .build(); } From 5dc9fabbf9f6505ef3083d7bd374815d655a8245 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Wed, 19 Nov 2025 16:03:00 -0400 Subject: [PATCH 3/8] CHANGELOG for Network Details extraction --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb788c78668..41bb6f63daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Features +- Enable capturing additional network details in your session replays (okhttp). + - Depends on `SentryOkHttpInterceptor` to intercept the request and extract request/response bodies. + - To enable, configure your sentry SDK using the "io.sentry.session-replay.network-*" options via [manifest](https://github.com/getsentry/sentry-java/blob/b03edbb1b0d8b871c62a09bc02cbd8a4e1f6fea1/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml#L196-L205) + - Or manually specify SentryReplayOptions via [SentryAndroid#init](https://github.com/getsentry/sentry-java/blob/c83e427e8baca17098f882f8b45fc7c5a80c1d8c/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java#L16-L28) - Implement OpenFeature Integration that tracks Feature Flag evaluations ([#4910](https://github.com/getsentry/sentry-java/pull/4910)) - To make use of it, add the `sentry-openfeature` dependency and register the the hook using: `openFeatureApiInstance.addHooks(new SentryOpenFeatureHook());` - Implement LaunchDarkly Integrations that track Feature Flag evaluations ([#4917](https://github.com/getsentry/sentry-java/pull/4917)) From 90164986783a3a189a006407e75c8f766bfcd33c Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 21 Nov 2025 13:26:03 -0400 Subject: [PATCH 4/8] unit tests ./gradlew :sentry-okhttp:test --tests="*SentryOkHttpEventTest*setNetworkDetails*" --- .../io/sentry/okhttp/SentryOkHttpEventTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index ff671ac153f..d93457de6f9 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -15,6 +15,7 @@ import io.sentry.TransactionContext import io.sentry.TypeCheckHint import io.sentry.exception.SentryHttpClientException import io.sentry.test.getProperty +import io.sentry.util.network.NetworkRequestData import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -425,6 +426,34 @@ class SentryOkHttpEventTest { verify(fixture.scopes, never()).captureEvent(any(), any()) } + @Test + fun `when finish is called, the breadcrumb sent includes network details data on its hint`() { + val sut = fixture.getSut() + val networkRequestData = NetworkRequestData("GET") + + sut.setNetworkDetails(networkRequestData) + sut.finish() + + verify(fixture.scopes) + .addBreadcrumb( + any(), + check { assertEquals(networkRequestData, it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) } + ) + } + + @Test + fun `when setNetworkDetails is not called, no network details data is captured`() { + val sut = fixture.getSut() + + sut.finish() + + verify(fixture.scopes) + .addBreadcrumb( + any(), + check { assertNull(it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) } + ) + } + /** Retrieve all the spans started in the event using reflection. */ private fun SentryOkHttpEvent.getEventDates() = getProperty>("eventDates") From e6d7289bd0b7582903e10e4ff0edc14e8e3ba3e7 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 21 Nov 2025 17:29:20 +0000 Subject: [PATCH 5/8] Format code --- .../src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index d93457de6f9..5570e37787b 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -437,7 +437,7 @@ class SentryOkHttpEventTest { verify(fixture.scopes) .addBreadcrumb( any(), - check { assertEquals(networkRequestData, it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) } + check { assertEquals(networkRequestData, it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) }, ) } @@ -450,7 +450,7 @@ class SentryOkHttpEventTest { verify(fixture.scopes) .addBreadcrumb( any(), - check { assertNull(it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) } + check { assertNull(it[TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS]) }, ) } From 4b30647f85b3d39af287a3446c25fa1d274fab12 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 21 Nov 2025 16:52:09 -0400 Subject: [PATCH 6/8] bug: fix NullPointerException if allowedHeaders contains null review comment - https://github.com/getsentry/sentry-java/pull/4919/files#r2550496731 seems possible, e.g. if a client passes null in the array to SentryReplayOptions#set[Request|Response]Headers --- .../network/NetworkDetailCaptureUtils.java | 4 +++- .../network/NetworkDetailCaptureUtilsTest.kt | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java b/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java index 21fcf2d0a35..1fd2b32b7cc 100644 --- a/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java +++ b/sentry/src/main/java/io/sentry/util/network/NetworkDetailCaptureUtils.java @@ -130,7 +130,9 @@ private static boolean shouldCaptureUrl( // Convert to lowercase for case-insensitive matching Set normalizedAllowed = new HashSet<>(); for (String header : allowedHeaders) { - normalizedAllowed.add(header.toLowerCase()); + if (header != null) { + normalizedAllowed.add(header.toLowerCase()); + } } for (Map.Entry entry : allHeaders.entrySet()) { diff --git a/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt b/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt index 94a5398acd5..c3b3faef61f 100644 --- a/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/network/NetworkDetailCaptureUtilsTest.kt @@ -78,4 +78,27 @@ class NetworkDetailCaptureUtilsTest { // Unwanted header should not be present assertTrue(!result.containsKey("X-Unwanted-Header")) } + + @Test + fun `getCaptureHeaders should handle null elements in allowedHeaders`() { + val allHeaders = + mapOf( + "Content-Type" to "application/json", + "Authorization" to "Bearer token123", + "X-Custom-Header" to "custom-value", + ) + + // allowedHeaders contains null elements which should be ignored + val allowedHeaders = arrayOf(null, "content-type", null, "authorization", null) + + val result = NetworkDetailCaptureUtils.getCaptureHeaders(allHeaders, allowedHeaders) + + // Only non-null allowed headers should be matched + assertEquals(2, result.size) + assertEquals("application/json", result["Content-Type"]) + assertEquals("Bearer token123", result["Authorization"]) + + // X-Custom-Header should not be present as it's not in the allowed list + assertTrue(!result.containsKey("X-Custom-Header")) + } } From af6765ded3d22adfbb29b97f4cf587594bc765ad Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 21 Nov 2025 17:48:50 -0400 Subject: [PATCH 7/8] bug: fix Duplicate HTTP headers lost in conversion review comment - https://github.com/getsentry/sentry-java/pull/4919#pullrequestreview-3484575241 ./gradlew :sentry-okhttp:test --tests="*SentryOkHttpInterceptorTest.toMap handles duplicate headers correctly*" --- .../sentry/okhttp/SentryOkHttpInterceptor.kt | 14 ++++++-- .../okhttp/SentryOkHttpInterceptorTest.kt | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index 5fa839ca891..2ede10d9e50 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -33,6 +33,7 @@ import okhttp3.Interceptor import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response +import org.jetbrains.annotations.VisibleForTesting /** * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span @@ -263,10 +264,19 @@ public open class SentryOkHttpInterceptor( } /** Extracts headers from OkHttp Headers object into a map */ - private fun okhttp3.Headers.toMap(): Map { + @VisibleForTesting + internal fun okhttp3.Headers.toMap(): Map { val headers = linkedMapOf() for (i in 0 until size) { - headers[name(i)] = value(i) + val name = name(i) + val value = value(i) + val existingValue = headers[name] + if (existingValue != null) { + // Concatenate duplicate headers with comma separator + headers[name] = "$existingValue, $value" + } else { + headers[name] = value + } } return headers } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index bbdb3a86516..a0d18c36e6b 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -680,4 +680,39 @@ class SentryOkHttpInterceptorTest { assertNotNull(recordedRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER)) assertNull(recordedRequest.getHeader(W3CTraceparentHeader.TRACEPARENT_HEADER)) } + + @Test + fun `toMap handles duplicate headers correctly`() { + // Create a response with duplicate headers + val mockResponse = + MockResponse() + .setResponseCode(200) + .setBody("test") + .addHeader("Set-Cookie", "sessionId=123") + .addHeader("Set-Cookie", "userId=456") + .addHeader("Set-Cookie", "theme=dark") + .addHeader("Accept", "text/html") + .addHeader("Accept", "application/json") + .addHeader("Single-Header", "value") + + fixture.server.enqueue(mockResponse) + + // Execute request to get response with headers + val sut = fixture.getSut() + val response = sut.newCall(getRequest()).execute() + val headers = response.headers + + // Optional: verify OkHttp preserves duplicate headers + assertEquals(3, headers.values("Set-Cookie").size) + assertEquals(2, headers.values("Accept").size) + assertEquals(1, headers.values("Single-Header").size) + + val interceptor = SentryOkHttpInterceptor(fixture.scopes) + val headerMap = with(interceptor) { headers.toMap() } + + // Duplicate headers will be collapsed into 1 concatenated entry with ", " separator + assertEquals("sessionId=123, userId=456, theme=dark", headerMap["Set-Cookie"]) + assertEquals("text/html, application/json", headerMap["Accept"]) + assertEquals("value", headerMap["Single-Header"]) + } } From 050c3a6133654fc059e629194474b3c4464ee647 Mon Sep 17 00:00:00 2001 From: Matthew Williams <43.matthew@gmail.com> Date: Fri, 21 Nov 2025 18:03:54 -0400 Subject: [PATCH 8/8] bug: fix Set-Cookie headers incorrectly concatenated with comma separator Issue is that commas are valid separators in certain headers (Cookie, Set-Cookie,...). Switch to semi-colon separated instead -> this only governs the formatted list that appears in the sentry dashboard so is relatively minor. review comment - https://github.com/getsentry/sentry-java/pull/4919#discussion_r2551063405 ./gradlew :sentry-okhttp:test --tests="*SentryOkHttpInterceptorTest.toMap handles duplicate headers correctly*" --- .../main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt | 4 ++-- .../java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index 2ede10d9e50..af7b25a782e 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -272,8 +272,8 @@ public open class SentryOkHttpInterceptor( val value = value(i) val existingValue = headers[name] if (existingValue != null) { - // Concatenate duplicate headers with comma separator - headers[name] = "$existingValue, $value" + // Concatenate duplicate headers with semicolon separator + headers[name] = "$existingValue; $value" } else { headers[name] = value } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index a0d18c36e6b..6e8b8548731 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -710,9 +710,9 @@ class SentryOkHttpInterceptorTest { val interceptor = SentryOkHttpInterceptor(fixture.scopes) val headerMap = with(interceptor) { headers.toMap() } - // Duplicate headers will be collapsed into 1 concatenated entry with ", " separator - assertEquals("sessionId=123, userId=456, theme=dark", headerMap["Set-Cookie"]) - assertEquals("text/html, application/json", headerMap["Accept"]) + // Duplicate headers will be collapsed into 1 concatenated entry with "; " separator + assertEquals("sessionId=123; userId=456; theme=dark", headerMap["Set-Cookie"]) + assertEquals("text/html; application/json", headerMap["Accept"]) assertEquals("value", headerMap["Single-Header"]) } }