Skip to content

Commit 84e8b86

Browse files
authored
Merge pull request #6240 from getsentry/fix/callback-error-handling-spring-user-provider
fix(spring): Handle user provider failures
2 parents db0ed76 + 5c639a8 commit 84e8b86

7 files changed

Lines changed: 175 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Keep Spring requests running when a `SentryUserProvider` throws and discard the incomplete user identity ([#6240](https://github.com/getsentry/sentry-java/pull/6240))
78
- Drop Apollo 5 spans when `beforeSpan` throws without disrupting the GraphQL request ([#6238](https://github.com/getsentry/sentry-java/pull/6238))
89

910
### Features

‎sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import io.sentry.IScope;
55
import io.sentry.IScopes;
66
import io.sentry.IpAddressUtils;
7+
import io.sentry.SentryLevel;
78
import io.sentry.protocol.User;
9+
import io.sentry.util.ExceptionUtils;
810
import io.sentry.util.Objects;
911
import jakarta.servlet.FilterChain;
1012
import jakarta.servlet.ServletException;
@@ -43,16 +45,30 @@ protected void doFilterInternal(
4345
final @NotNull FilterChain chain)
4446
throws ServletException, IOException {
4547
final User user = new User();
48+
boolean providerFailed = false;
4649
for (final SentryUserProvider provider : sentryUserProviders) {
47-
apply(user, provider.provideUser());
50+
try {
51+
apply(user, provider.provideUser());
52+
} catch (Throwable e) {
53+
ExceptionUtils.rethrowIfFatal(e);
54+
providerFailed = true;
55+
scopes
56+
.getOptions()
57+
.getLogger()
58+
.log(
59+
SentryLevel.ERROR,
60+
e,
61+
"The SentryUserProvider callback %s threw an exception.",
62+
provider.getClass().getName());
63+
}
4864
}
49-
if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
65+
if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
5066
if (IpAddressUtils.isDefault(user.getIpAddress())) {
5167
// unset {{auto}} as it would set the server's ip address as a user ip address
5268
user.setIpAddress(null);
5369
}
5470
}
55-
scopes.setUser(user);
71+
scopes.setUser(providerFailed ? new User() : user);
5672
chain.doFilter(request, response);
5773
}
5874

‎sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import io.sentry.protocol.User
66
import jakarta.servlet.FilterChain
77
import kotlin.test.Test
88
import kotlin.test.assertEquals
9+
import kotlin.test.assertFailsWith
910
import kotlin.test.assertNull
11+
import org.assertj.core.api.Assertions.assertThat
1012
import org.mockito.kotlin.check
1113
import org.mockito.kotlin.mock
14+
import org.mockito.kotlin.never
1215
import org.mockito.kotlin.verify
1316
import org.mockito.kotlin.whenever
1417
import org.springframework.mock.web.MockHttpServletRequest
@@ -151,6 +154,42 @@ class SentryUserFilterTest {
151154
verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) })
152155
}
153156

157+
@Test
158+
fun `provider failure discards user data while later providers and request continue`() {
159+
val failure = RuntimeException("provider failed")
160+
val laterProvider = mock<SentryUserProvider>()
161+
whenever(laterProvider.provideUser()).thenReturn(sampleUser)
162+
val filter =
163+
fixture.getSut(
164+
userProviders =
165+
listOf(
166+
SentryUserProvider { sampleUser },
167+
SentryUserProvider { throw failure },
168+
laterProvider,
169+
)
170+
)
171+
172+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
173+
174+
verify(fixture.scopes).setUser(check { assertEquals(User(), it) })
175+
verify(laterProvider).provideUser()
176+
verify(fixture.chain).doFilter(fixture.request, fixture.response)
177+
}
178+
179+
@Test
180+
fun `fatal provider failure propagates`() {
181+
val failure = OutOfMemoryError("fatal")
182+
val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure }))
183+
184+
assertThat(
185+
assertFailsWith<OutOfMemoryError> {
186+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
187+
}
188+
)
189+
.isSameAs(failure)
190+
verify(fixture.chain, never()).doFilter(fixture.request, fixture.response)
191+
}
192+
154193
private fun assertEquals(user1: User, user2: User) {
155194
assertEquals(user1.username, user2.username)
156195
assertEquals(user1.id, user2.id)

‎sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import io.sentry.IScope;
55
import io.sentry.IScopes;
66
import io.sentry.IpAddressUtils;
7+
import io.sentry.SentryLevel;
78
import io.sentry.protocol.User;
9+
import io.sentry.util.ExceptionUtils;
810
import io.sentry.util.Objects;
911
import jakarta.servlet.FilterChain;
1012
import jakarta.servlet.ServletException;
@@ -43,16 +45,30 @@ protected void doFilterInternal(
4345
final @NotNull FilterChain chain)
4446
throws ServletException, IOException {
4547
final User user = new User();
48+
boolean providerFailed = false;
4649
for (final SentryUserProvider provider : sentryUserProviders) {
47-
apply(user, provider.provideUser());
50+
try {
51+
apply(user, provider.provideUser());
52+
} catch (Throwable e) {
53+
ExceptionUtils.rethrowIfFatal(e);
54+
providerFailed = true;
55+
scopes
56+
.getOptions()
57+
.getLogger()
58+
.log(
59+
SentryLevel.ERROR,
60+
e,
61+
"The SentryUserProvider callback %s threw an exception.",
62+
provider.getClass().getName());
63+
}
4864
}
49-
if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
65+
if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
5066
if (IpAddressUtils.isDefault(user.getIpAddress())) {
5167
// unset {{auto}} as it would set the server's ip address as a user ip address
5268
user.setIpAddress(null);
5369
}
5470
}
55-
scopes.setUser(user);
71+
scopes.setUser(providerFailed ? new User() : user);
5672
chain.doFilter(request, response);
5773
}
5874

‎sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import io.sentry.protocol.User
66
import jakarta.servlet.FilterChain
77
import kotlin.test.Test
88
import kotlin.test.assertEquals
9+
import kotlin.test.assertFailsWith
910
import kotlin.test.assertNull
11+
import org.assertj.core.api.Assertions.assertThat
1012
import org.mockito.kotlin.check
1113
import org.mockito.kotlin.mock
14+
import org.mockito.kotlin.never
1215
import org.mockito.kotlin.verify
1316
import org.mockito.kotlin.whenever
1417
import org.springframework.mock.web.MockHttpServletRequest
@@ -151,6 +154,42 @@ class SentryUserFilterTest {
151154
verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) })
152155
}
153156

157+
@Test
158+
fun `provider failure discards user data while later providers and request continue`() {
159+
val failure = RuntimeException("provider failed")
160+
val laterProvider = mock<SentryUserProvider>()
161+
whenever(laterProvider.provideUser()).thenReturn(sampleUser)
162+
val filter =
163+
fixture.getSut(
164+
userProviders =
165+
listOf(
166+
SentryUserProvider { sampleUser },
167+
SentryUserProvider { throw failure },
168+
laterProvider,
169+
)
170+
)
171+
172+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
173+
174+
verify(fixture.scopes).setUser(check { assertEquals(User(), it) })
175+
verify(laterProvider).provideUser()
176+
verify(fixture.chain).doFilter(fixture.request, fixture.response)
177+
}
178+
179+
@Test
180+
fun `fatal provider failure propagates`() {
181+
val failure = OutOfMemoryError("fatal")
182+
val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure }))
183+
184+
assertThat(
185+
assertFailsWith<OutOfMemoryError> {
186+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
187+
}
188+
)
189+
.isSameAs(failure)
190+
verify(fixture.chain, never()).doFilter(fixture.request, fixture.response)
191+
}
192+
154193
private fun assertEquals(user1: User, user2: User) {
155194
assertEquals(user1.username, user2.username)
156195
assertEquals(user1.id, user2.id)

‎sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import io.sentry.IScope;
55
import io.sentry.IScopes;
66
import io.sentry.IpAddressUtils;
7+
import io.sentry.SentryLevel;
78
import io.sentry.protocol.User;
9+
import io.sentry.util.ExceptionUtils;
810
import io.sentry.util.Objects;
911
import java.io.IOException;
1012
import java.util.List;
@@ -43,16 +45,30 @@ protected void doFilterInternal(
4345
final @NotNull FilterChain chain)
4446
throws ServletException, IOException {
4547
final User user = new User();
48+
boolean providerFailed = false;
4649
for (final SentryUserProvider provider : sentryUserProviders) {
47-
apply(user, provider.provideUser());
50+
try {
51+
apply(user, provider.provideUser());
52+
} catch (Throwable e) {
53+
ExceptionUtils.rethrowIfFatal(e);
54+
providerFailed = true;
55+
scopes
56+
.getOptions()
57+
.getLogger()
58+
.log(
59+
SentryLevel.ERROR,
60+
e,
61+
"The SentryUserProvider callback %s threw an exception.",
62+
provider.getClass().getName());
63+
}
4864
}
49-
if (scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
65+
if (!providerFailed && scopes.getOptions().getDataCollectionResolver().isUserInfo()) {
5066
if (IpAddressUtils.isDefault(user.getIpAddress())) {
5167
// unset {{auto}} as it would set the server's ip address as a user ip address
5268
user.setIpAddress(null);
5369
}
5470
}
55-
scopes.setUser(user);
71+
scopes.setUser(providerFailed ? new User() : user);
5672
chain.doFilter(request, response);
5773
}
5874

‎sentry-spring/src/test/kotlin/io/sentry/spring/SentryUserFilterTest.kt‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import io.sentry.protocol.User
66
import javax.servlet.FilterChain
77
import kotlin.test.Test
88
import kotlin.test.assertEquals
9+
import kotlin.test.assertFailsWith
910
import kotlin.test.assertNull
11+
import org.assertj.core.api.Assertions.assertThat
1012
import org.mockito.kotlin.check
1113
import org.mockito.kotlin.mock
14+
import org.mockito.kotlin.never
1215
import org.mockito.kotlin.verify
1316
import org.mockito.kotlin.whenever
1417
import org.springframework.mock.web.MockHttpServletRequest
@@ -151,6 +154,42 @@ class SentryUserFilterTest {
151154
verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) })
152155
}
153156

157+
@Test
158+
fun `provider failure discards user data while later providers and request continue`() {
159+
val failure = RuntimeException("provider failed")
160+
val laterProvider = mock<SentryUserProvider>()
161+
whenever(laterProvider.provideUser()).thenReturn(sampleUser)
162+
val filter =
163+
fixture.getSut(
164+
userProviders =
165+
listOf(
166+
SentryUserProvider { sampleUser },
167+
SentryUserProvider { throw failure },
168+
laterProvider,
169+
)
170+
)
171+
172+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
173+
174+
verify(fixture.scopes).setUser(check { assertEquals(User(), it) })
175+
verify(laterProvider).provideUser()
176+
verify(fixture.chain).doFilter(fixture.request, fixture.response)
177+
}
178+
179+
@Test
180+
fun `fatal provider failure propagates`() {
181+
val failure = OutOfMemoryError("fatal")
182+
val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { throw failure }))
183+
184+
assertThat(
185+
assertFailsWith<OutOfMemoryError> {
186+
filter.doFilter(fixture.request, fixture.response, fixture.chain)
187+
}
188+
)
189+
.isSameAs(failure)
190+
verify(fixture.chain, never()).doFilter(fixture.request, fixture.response)
191+
}
192+
154193
private fun assertEquals(user1: User, user2: User) {
155194
assertEquals(user1.username, user2.username)
156195
assertEquals(user1.id, user2.id)

0 commit comments

Comments
 (0)