Skip to content
Open
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
1 change: 1 addition & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7706,6 +7706,7 @@ public final class io/sentry/util/ExceptionUtils {
public fun <init> ()V
public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable;
public static fun isIgnored (Ljava/util/Set;Ljava/lang/Throwable;)Z
public static fun rethrowIfFatal (Ljava/lang/Throwable;)V
}

public final class io/sentry/util/FileUtils {
Expand Down
30 changes: 30 additions & 0 deletions sentry/src/main/java/io/sentry/util/ExceptionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,34 @@ public static boolean isIgnored(
final @NotNull Throwable throwable) {
return ignoredExceptionsForType.contains(throwable.getClass());
}

/**
* Rethrows non-recoverable {@link Throwable}s that should never be swallowed: {@link
* VirtualMachineError} (e.g. OutOfMemoryError/StackOverflowError), {@link ThreadDeath} and {@link
* LinkageError} are rethrown as-is. For {@link InterruptedException}, the thread's interrupted
* status is restored instead of rethrowing, since it is a checked exception. All other throwables
* are left untouched for the caller to handle/log/ignore as before.
*
* <p>Note on {@link LinkageError}: its subclasses (e.g. {@link NoClassDefFoundError}, {@link
* NoSuchMethodError}, {@link AbstractMethodError}, {@link UnsatisfiedLinkError}) are also the
* expected runtime signal for a missing or version-mismatched optional dependency, which is a
* normal condition for integrations built against {@code compileOnly} dependencies. Call sites
* that probe for such a dependency must catch the relevant {@link LinkageError} subclass locally
* <em>before</em> delegating to this method — see {@code SentrySQLiteDriver.hasConnectionPool}
* and {@link LoadClass} for examples.
*
* @param throwable - the throwable to check
*/
public static void rethrowIfFatal(final @NotNull Throwable throwable) {
// VirtualMachineError covers OutOfMemoryError, StackOverflowError, InternalError, and
// UnknownError
if (throwable instanceof VirtualMachineError
|| throwable instanceof ThreadDeath
|| throwable instanceof LinkageError) {
throw (Error) throwable;
}
if (throwable instanceof InterruptedException) {
Comment thread
markushi marked this conversation as resolved.
Thread.currentThread().interrupt();
}
}
}
58 changes: 58 additions & 0 deletions sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package io.sentry.util

import com.google.common.truth.Truth.assertThat
import java.lang.RuntimeException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class ExceptionUtilsTest {
@Test
Expand All @@ -18,4 +23,57 @@ class ExceptionUtilsTest {
val ex = RuntimeException(cause)
assertEquals(rootCause, ExceptionUtils.findRootCause(ex))
}

@Test
fun `rethrowIfFatal rethrows OutOfMemoryError`() {
assertFails { ExceptionUtils.rethrowIfFatal(OutOfMemoryError()) }
}

@Test
fun `rethrowIfFatal rethrows StackOverflowError`() {
assertFails { ExceptionUtils.rethrowIfFatal(StackOverflowError()) }
}

@Test
fun `rethrowIfFatal rethrows ThreadDeath`() {
assertFails { ExceptionUtils.rethrowIfFatal(ThreadDeath()) }
}

@Test
fun `rethrowIfFatal rethrows NoClassDefFoundError as-is`() {
val error = NoClassDefFoundError()
val thrown = assertFailsWith<NoClassDefFoundError> { ExceptionUtils.rethrowIfFatal(error) }
assertThat(thrown).isSameInstanceAs(error)
}

@Test
fun `rethrowIfFatal rethrows NoSuchMethodError as-is`() {
val error = NoSuchMethodError()
val thrown = assertFailsWith<NoSuchMethodError> { ExceptionUtils.rethrowIfFatal(error) }
assertThat(thrown).isSameInstanceAs(error)
}

@Test
fun `rethrowIfFatal rethrows UnsatisfiedLinkError as-is`() {
val error = UnsatisfiedLinkError()
val thrown = assertFailsWith<UnsatisfiedLinkError> { ExceptionUtils.rethrowIfFatal(error) }
assertThat(thrown).isSameInstanceAs(error)
}

@Test
fun `rethrowIfFatal restores interrupt flag for InterruptedException without rethrowing`() {
try {
ExceptionUtils.rethrowIfFatal(InterruptedException())
assertTrue(Thread.currentThread().isInterrupted)
} finally {
// clear the interrupt flag so it doesn't leak into other tests
Thread.interrupted()
}
}

@Test
fun `rethrowIfFatal does nothing for regular exceptions`() {
ExceptionUtils.rethrowIfFatal(RuntimeException())
assertFalse(Thread.currentThread().isInterrupted)
}
}
Loading