From 805d7ea4c4bf72d968f9ca98090c67eda2d4c399 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 9 Oct 2026 06:26:19 +0200 Subject: [PATCH 1/3] fix(spring): Handle user provider failures Keep requests running when a Spring user provider throws. Continue invoking later providers, but discard all provider-derived identity and install an empty user so inherited identity cannot leak into the event. Co-Authored-By: Claude --- .../io/sentry/spring7/SentryUserFilter.java | 18 +++++++-- .../io/sentry/spring7/SentryUserFilterTest.kt | 39 +++++++++++++++++++ .../spring/jakarta/SentryUserFilter.java | 18 +++++++-- .../spring/jakarta/SentryUserFilterTest.kt | 39 +++++++++++++++++++ .../io/sentry/spring/SentryUserFilter.java | 18 +++++++-- .../io/sentry/spring/SentryUserFilterTest.kt | 39 +++++++++++++++++++ 6 files changed, 162 insertions(+), 9 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java index da9d2f0f77e..ac592cb9ae8 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java @@ -4,7 +4,9 @@ import io.sentry.IScope; import io.sentry.IScopes; import io.sentry.IpAddressUtils; +import io.sentry.SentryLevel; import io.sentry.protocol.User; +import io.sentry.util.ExceptionUtils; import io.sentry.util.Objects; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -43,16 +45,26 @@ protected void doFilterInternal( final @NotNull FilterChain chain) throws ServletException, IOException { final User user = new User(); + boolean providerFailed = false; for (final SentryUserProvider provider : sentryUserProviders) { - apply(user, provider.provideUser()); + try { + apply(user, provider.provideUser()); + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + providerFailed = true; + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + } } - if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) { + if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) { if (IpAddressUtils.isDefault(user.getIpAddress())) { // unset {{auto}} as it would set the server's ip address as a user ip address user.setIpAddress(null); } } - scopes.setUser(user); + scopes.setUser(providerFailed ? new User() : user); chain.doFilter(request, response); } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt index 92327456e13..8d6a2fbcf69 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt @@ -6,9 +6,12 @@ import io.sentry.protocol.User import jakarta.servlet.FilterChain import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull +import org.assertj.core.api.Assertions.assertThat import org.mockito.kotlin.check import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.mock.web.MockHttpServletRequest @@ -151,6 +154,42 @@ class SentryUserFilterTest { verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) }) } + @Test + fun `provider failure discards user data while later providers and request continue`() { + val failure = RuntimeException("provider failed") + val laterProvider = mock() + whenever(laterProvider.provideUser()).thenReturn(sampleUser) + val filter = + fixture.getSut( + userProviders = + listOf( + SentryUserProvider { sampleUser }, + SentryUserProvider { throw failure }, + laterProvider, + ) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(User(), it) }) + verify(laterProvider).provideUser() + verify(fixture.chain).doFilter(fixture.request, fixture.response) + } + + @Test + fun `fatal provider failure propagates`() { + val failure = OutOfMemoryError("fatal") + val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure })) + + assertThat( + assertFailsWith { + filter.doFilter(fixture.request, fixture.response, fixture.chain) + } + ) + .isSameAs(failure) + verify(fixture.chain, never()).doFilter(fixture.request, fixture.response) + } + private fun assertEquals(user1: User, user2: User) { assertEquals(user1.username, user2.username) assertEquals(user1.id, user2.id) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java index 23a77f79f0d..ab05404fb18 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java @@ -4,7 +4,9 @@ import io.sentry.IScope; import io.sentry.IScopes; import io.sentry.IpAddressUtils; +import io.sentry.SentryLevel; import io.sentry.protocol.User; +import io.sentry.util.ExceptionUtils; import io.sentry.util.Objects; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -43,16 +45,26 @@ protected void doFilterInternal( final @NotNull FilterChain chain) throws ServletException, IOException { final User user = new User(); + boolean providerFailed = false; for (final SentryUserProvider provider : sentryUserProviders) { - apply(user, provider.provideUser()); + try { + apply(user, provider.provideUser()); + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + providerFailed = true; + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + } } - if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) { + if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) { if (IpAddressUtils.isDefault(user.getIpAddress())) { // unset {{auto}} as it would set the server's ip address as a user ip address user.setIpAddress(null); } } - scopes.setUser(user); + scopes.setUser(providerFailed ? new User() : user); chain.doFilter(request, response); } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt index 15a7bf377cd..1cb91b1445f 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt @@ -6,9 +6,12 @@ import io.sentry.protocol.User import jakarta.servlet.FilterChain import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull +import org.assertj.core.api.Assertions.assertThat import org.mockito.kotlin.check import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.mock.web.MockHttpServletRequest @@ -151,6 +154,42 @@ class SentryUserFilterTest { verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) }) } + @Test + fun `provider failure discards user data while later providers and request continue`() { + val failure = RuntimeException("provider failed") + val laterProvider = mock() + whenever(laterProvider.provideUser()).thenReturn(sampleUser) + val filter = + fixture.getSut( + userProviders = + listOf( + SentryUserProvider { sampleUser }, + SentryUserProvider { throw failure }, + laterProvider, + ) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(User(), it) }) + verify(laterProvider).provideUser() + verify(fixture.chain).doFilter(fixture.request, fixture.response) + } + + @Test + fun `fatal provider failure propagates`() { + val failure = OutOfMemoryError("fatal") + val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure })) + + assertThat( + assertFailsWith { + filter.doFilter(fixture.request, fixture.response, fixture.chain) + } + ) + .isSameAs(failure) + verify(fixture.chain, never()).doFilter(fixture.request, fixture.response) + } + private fun assertEquals(user1: User, user2: User) { assertEquals(user1.username, user2.username) assertEquals(user1.id, user2.id) diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java index 18e1c0d2875..ed7f8071076 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java @@ -4,7 +4,9 @@ import io.sentry.IScope; import io.sentry.IScopes; import io.sentry.IpAddressUtils; +import io.sentry.SentryLevel; import io.sentry.protocol.User; +import io.sentry.util.ExceptionUtils; import io.sentry.util.Objects; import java.io.IOException; import java.util.List; @@ -43,16 +45,26 @@ protected void doFilterInternal( final @NotNull FilterChain chain) throws ServletException, IOException { final User user = new User(); + boolean providerFailed = false; for (final SentryUserProvider provider : sentryUserProviders) { - apply(user, provider.provideUser()); + try { + apply(user, provider.provideUser()); + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + providerFailed = true; + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + } } - if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) { + if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) { if (IpAddressUtils.isDefault(user.getIpAddress())) { // unset {{auto}} as it would set the server's ip address as a user ip address user.setIpAddress(null); } } - scopes.setUser(user); + scopes.setUser(providerFailed ? new User() : user); chain.doFilter(request, response); } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryUserFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryUserFilterTest.kt index 07283bd5b95..8c0ed0f2f92 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryUserFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryUserFilterTest.kt @@ -6,9 +6,12 @@ import io.sentry.protocol.User import javax.servlet.FilterChain import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull +import org.assertj.core.api.Assertions.assertThat import org.mockito.kotlin.check import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.springframework.mock.web.MockHttpServletRequest @@ -151,6 +154,42 @@ class SentryUserFilterTest { verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) }) } + @Test + fun `provider failure discards user data while later providers and request continue`() { + val failure = RuntimeException("provider failed") + val laterProvider = mock() + whenever(laterProvider.provideUser()).thenReturn(sampleUser) + val filter = + fixture.getSut( + userProviders = + listOf( + SentryUserProvider { sampleUser }, + SentryUserProvider { throw failure }, + laterProvider, + ) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(User(), it) }) + verify(laterProvider).provideUser() + verify(fixture.chain).doFilter(fixture.request, fixture.response) + } + + @Test + fun `fatal provider failure propagates`() { + val failure = OutOfMemoryError("fatal") + val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure })) + + assertThat( + assertFailsWith { + filter.doFilter(fixture.request, fixture.response, fixture.chain) + } + ) + .isSameAs(failure) + verify(fixture.chain, never()).doFilter(fixture.request, fixture.response) + } + private fun assertEquals(user1: User, user2: User) { assertEquals(user1.username, user2.username) assertEquals(user1.id, user2.id) From 22bc0e6a56541c05b866c4dfd5cb210f360de479 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 9 Oct 2026 06:26:45 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc054e0be18..40bffe0ec5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Keep Spring requests running when a `SentryUserProvider` throws and discard the incomplete user identity ([#6240](https://github.com/getsentry/sentry-java/pull/6240)) + ### Features - Add support for Android Navigation 3 through the new `sentry-android-navigation3` library ([#6233](https://github.com/getsentry/sentry-java/pull/6233)) From 27184c688ea4ceafb071861c53ed02bcad4daf7f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 9 Oct 2026 16:35:27 +0200 Subject: [PATCH 3/3] fix(spring): Include user provider name in error log Identify which SentryUserProvider failed when multiple providers are configured, making callback failures actionable across all Spring variants. Refs GH-6240 Co-Authored-By: Claude --- .../src/main/java/io/sentry/spring7/SentryUserFilter.java | 6 +++++- .../java/io/sentry/spring/jakarta/SentryUserFilter.java | 6 +++++- .../src/main/java/io/sentry/spring/SentryUserFilter.java | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java index ac592cb9ae8..5b4afe069df 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java @@ -55,7 +55,11 @@ protected void doFilterInternal( scopes .getOptions() .getLogger() - .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + .log( + SentryLevel.ERROR, + e, + "The SentryUserProvider callback %s threw an exception.", + provider.getClass().getName()); } } if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java index ab05404fb18..796dee9da7e 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java @@ -55,7 +55,11 @@ protected void doFilterInternal( scopes .getOptions() .getLogger() - .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + .log( + SentryLevel.ERROR, + e, + "The SentryUserProvider callback %s threw an exception.", + provider.getClass().getName()); } } if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) { diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java index ed7f8071076..00b2ed9b62f 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java @@ -55,7 +55,11 @@ protected void doFilterInternal( scopes .getOptions() .getLogger() - .log(SentryLevel.ERROR, "The SentryUserProvider callback threw an exception.", e); + .log( + SentryLevel.ERROR, + e, + "The SentryUserProvider callback %s threw an exception.", + provider.getClass().getName()); } } if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) {