From 09675f9f7895fda5c5741bf42ca9df327b658128 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 11 Nov 2026 15:31:35 +0100 Subject: [PATCH 1/2] fix: Narrow Throwable catches where LinkageError is expected ExceptionUtils.rethrowIfFatal rethrows LinkageError, but at these sites a LinkageError is the expected failure: a native library that failed to load, an optional dependency that is missing, or a Compose or androidx version that doesn't match. Catch it explicitly first, and narrow the rest to the types the guarded code throws. Where the code calls app views, user integrations or JNI, keep the broad catch and call rethrowIfFatal after the LinkageError arm. Also stop QueueFile from deleting its file on an OutOfMemoryError, stop Installation from wrapping one in a RuntimeException, and stop the Ktor client integration from swallowing coroutine cancellation while reading the response body. Refs JAVA-552 Co-Authored-By: Claude Opus 5.5 --- .../java/io/sentry/android/core/AppState.java | 5 ++- .../io/sentry/android/core/Installation.java | 2 +- .../sentry/android/core/NdkIntegration.java | 6 ++- .../core/ViewHierarchyEventProcessor.java | 14 ++++++ .../gestures/SentryWindowCallback.java | 10 +++++ .../sentry/android/ndk/DebugImagesLoader.java | 13 ++++++ .../java/io/sentry/android/ndk/SentryNdk.java | 6 +-- .../java/io/sentry/android/replay/Windows.kt | 9 ++-- .../io/sentry/android/replay/util/Nodes.kt | 8 +++- .../viewhierarchy/ComposeViewHierarchyNode.kt | 42 +++++++++++++----- .../io/sentry/compose/SentryComposeHelper.kt | 6 ++- .../ktorClient/SentryKtorClientUtils.kt | 9 +++- .../SentryContextStorageProvider.java | 6 ++- sentry/src/main/java/io/sentry/Sentry.java | 15 +++++++ .../main/java/io/sentry/SentryOptions.java | 4 +- .../UncaughtExceptionHandlerIntegration.java | 3 ++ .../java/io/sentry/cache/tape/QueueFile.java | 2 +- .../profiling/ProfilingServiceLoader.java | 7 ++- .../main/java/io/sentry/util/LoadClass.java | 3 +- sentry/src/test/java/io/sentry/SentryTest.kt | 44 +++++++++++++++++++ .../test/java/io/sentry/util/LoadClassTest.kt | 11 +++++ 21 files changed, 193 insertions(+), 32 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java index 74522f7aaac..228a5909ae8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java @@ -112,7 +112,8 @@ private void ensureLifecycleObserver(final @NotNull ILogger logger) { SentryLevel.WARNING, "androidx.lifecycle is not available, some features might not be properly working," + "e.g. Session Tracking, Network and System Events breadcrumbs, etc."); - } catch (Throwable e) { + } catch (LinkageError e) { + // androidx.lifecycle has an incompatible version logger.log(SentryLevel.ERROR, "AppState could not register lifecycle observer", e); } } @@ -124,7 +125,7 @@ private void addObserverInternal(final @NotNull ILogger logger) { if (observerRef != null) { ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef); } - } catch (Throwable e) { + } catch (LinkageError e) { // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in // connection with conflicting dependencies of the androidx.lifecycle. // //See the issue here: https://github.com/getsentry/sentry-java/pull/2228 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java index ba08e71342a..8f85a1ef4f1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java @@ -44,7 +44,7 @@ public static String id(final @NotNull Context context) throws RuntimeException return deviceId; } deviceId = readInstallationFile(installation); - } catch (Throwable e) { + } catch (IOException e) { throw new RuntimeException(e); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index 8353a32d65d..7bac0d63a68 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -62,7 +62,9 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO this.options .getLogger() .log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.init method.", e); - } catch (Throwable e) { + } catch (ReflectiveOperationException e) { + // InvocationTargetException wraps whatever SentryNdk.init threw, such as an + // UnsatisfiedLinkError when the native library failed to load disableNdkIntegration(this.options); this.options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e); } @@ -94,7 +96,7 @@ public void close() throws IOException { options .getLogger() .log(SentryLevel.ERROR, "Failed to invoke the SentryNdk.close method.", e); - } catch (Throwable e) { + } catch (ReflectiveOperationException e) { options.getLogger().log(SentryLevel.ERROR, "Failed to close SentryNdk.", e); } finally { disableNdkIntegration(this.options); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index ffd3323e601..096bd26858e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -196,7 +196,14 @@ public static ViewHierarchy snapshotViewHierarchy( try { viewHierarchy.set(snapshotViewHierarchy(decorView, exporters)); latch.countDown(); + } catch (LinkageError e) { + // The Compose exporter calls Compose internals, which may not match the app's + // Compose version + logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", e); } catch (Throwable t) { + // The snapshot calls into the app's own views, so we can't know what it throws. + // The event is sent without a view hierarchy. + ExceptionUtils.rethrowIfFatal(t); logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t); } }); @@ -204,7 +211,14 @@ public static ViewHierarchy snapshotViewHierarchy( return viewHierarchy.get(); } } + } catch (LinkageError e) { + // The Compose exporter calls Compose internals, which may not match the app's Compose + // version + logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", e); } catch (Throwable t) { + // The snapshot calls into the app's own views, so we can't know what it throws. The event + // is sent without a view hierarchy. + ExceptionUtils.rethrowIfFatal(t); logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t); } return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java index 612eb97946e..7a3e6b42302 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryWindowCallback.java @@ -6,6 +6,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanStatus; +import io.sentry.util.ExceptionUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,16 @@ public boolean dispatchTouchEvent(final @Nullable MotionEvent motionEvent) { final MotionEvent copy = motionEventObtainer.obtain(motionEvent); try { handleTouchEvent(copy); + } catch (LinkageError e) { + // The Compose gesture locator calls Compose internals, which may not match the app's + // Compose version + if (options != null) { + options.getLogger().log(SentryLevel.ERROR, "Error dispatching touch event", e); + } } catch (Throwable e) { + // Locating the touched view calls into the app's own views, so we can't know what it + // throws. The gesture is not recorded, and the touch still reaches the app. + ExceptionUtils.rethrowIfFatal(e); if (options != null) { options.getLogger().log(SentryLevel.ERROR, "Error dispatching touch event", e); } diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java index 602981c1d01..af817abe722 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java @@ -8,6 +8,7 @@ import io.sentry.ndk.NativeModuleListLoader; import io.sentry.protocol.DebugImage; import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.ExceptionUtils; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.HashSet; @@ -71,7 +72,13 @@ public DebugImagesLoader( .getLogger() .log(SentryLevel.DEBUG, "Debug images loaded: %d", debugImages.size()); } + } catch (LinkageError e) { + // e.g. UnsatisfiedLinkError when the native library failed to load + options.getLogger().log(SentryLevel.ERROR, e, "Failed to load debug images."); } catch (Throwable e) { + // A JNI call into sentry-native, so we can't know what it throws. The event is sent + // without debug images. + ExceptionUtils.rethrowIfFatal(e); options.getLogger().log(SentryLevel.ERROR, e, "Failed to load debug images."); } } @@ -172,7 +179,13 @@ public void clearDebugImages() { moduleListLoader.clearModuleList(); options.getLogger().log(SentryLevel.INFO, "Debug images cleared."); + } catch (LinkageError e) { + // e.g. UnsatisfiedLinkError when the native library failed to load + options.getLogger().log(SentryLevel.ERROR, e, "Failed to clear debug images."); } catch (Throwable e) { + // A JNI call into sentry-native, so we can't know what it throws. The cached list is + // cleared either way. + ExceptionUtils.rethrowIfFatal(e); options.getLogger().log(SentryLevel.ERROR, e, "Failed to clear debug images."); } debugImages = null; diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index fae652f7a64..c182ba6ca7e 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -24,9 +24,9 @@ private SentryNdk() {} try { //noinspection UnstableApiUsage io.sentry.ndk.SentryNdk.loadNativeLibraries(); - } catch (Throwable t) { - // ignored - // if loadLibrary() fails, the later init() will throw an exception anyway + } catch (LinkageError ignored) { + // e.g. UnsatisfiedLinkError. The later init() throws too, which disables the NDK + // integration. } finally { loadLibraryLatch.countDown(); } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt index 4f8f6cabf58..e1586738935 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt @@ -53,7 +53,7 @@ internal object WindowSpy { lazy(NONE) { try { Class.forName("com.android.internal.policy.DecorView") - } catch (ignored: Throwable) { + } catch (ignored: ClassNotFoundException) { Log.d("WindowSpy", "Unexpected exception loading DecorView on API $SDK_INT", ignored) null } @@ -171,7 +171,7 @@ internal object WindowManagerSpy { val className = "android.view.WindowManagerGlobal" try { Class.forName(className) - } catch (ignored: Throwable) { + } catch (ignored: ClassNotFoundException) { Log.w("WindowManagerSpy", ignored) null } @@ -198,7 +198,10 @@ internal object WindowManagerSpy { mViewsField[windowManagerInstance] = swap(mViews) } } - } catch (ignored: Throwable) { + } catch (ignored: ReflectiveOperationException) { + Log.w("WindowManagerSpy", ignored) + } catch (ignored: ClassCastException) { + // mViews is not an ArrayList on this device Log.w("WindowManagerSpy", ignored) } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt index 704260cf311..66372e7ba77 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.findRootCoordinates import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.text.TextLayoutResult +import io.sentry.util.ExceptionUtils import kotlin.math.ceil import kotlin.math.floor import kotlin.math.roundToInt @@ -93,7 +94,7 @@ internal fun LayoutNode.findPainter(): Painter? { return try { modifier::class.java.getDeclaredField("painter").apply { isAccessible = true }.get(modifier) as? Painter - } catch (e: Throwable) { + } catch (_: ReflectiveOperationException) { null } } @@ -136,7 +137,12 @@ internal fun LayoutNode.findTextColor(): Color? { (modifier::class.java.getDeclaredField("color").apply { isAccessible = true }.get(modifier) as? ColorProducer) ?.invoke() + } catch (_: LinkageError) { + // older Compose versions don't have ColorProducer + null } catch (e: Throwable) { + // ColorProducer.invoke() runs the app's color lambda, so we can't know what it throws + ExceptionUtils.rethrowIfFatal(e) null } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index 2e40144e2de..cf8b8c69eac 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -32,6 +32,7 @@ import io.sentry.android.replay.util.toRect import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode +import io.sentry.util.ExceptionUtils import java.lang.ref.WeakReference import java.lang.reflect.Method @@ -45,7 +46,7 @@ internal object ComposeViewHierarchyNode { .java .getDeclaredMethod("getCollapsedSemantics\$ui_release") .apply { isAccessible = true } - } catch (_: Throwable) { + } catch (_: NoSuchMethodException) { // ignore, as this method may not be available } return@lazy null @@ -57,7 +58,7 @@ internal object ComposeViewHierarchyNode { internal fun retrieveSemanticsConfiguration(node: LayoutNode): SemanticsConfiguration? { return try { node.semanticsConfiguration - } catch (t: Throwable) { + } catch (t: NoSuchMethodError) { // for backwards compatibility // Jetpack Compose 1.8 or older if (getCollapsedSemanticsMethod != null) { @@ -128,16 +129,27 @@ internal object ComposeViewHierarchyNode { } val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates?.get()) - val semantics: SemanticsConfiguration? + var semanticsFailure: Throwable? = null + val semantics: SemanticsConfiguration? = + try { + retrieveSemanticsConfiguration(node) + } catch (e: LinkageError) { + // an unsupported Compose version, which must not crash the app + semanticsFailure = e + null + } catch (t: Throwable) { + // semantics modifiers run app code, so we can't know what this throws + ExceptionUtils.rethrowIfFatal(t) + semanticsFailure = t + null + } - try { - semantics = retrieveSemanticsConfiguration(node) - } catch (t: Throwable) { + if (semanticsFailure != null) { if (!semanticsRetrievalErrorLogged) { semanticsRetrievalErrorLogged = true logger.log( SentryLevel.ERROR, - t, + semanticsFailure, """ Error retrieving semantics information from Compose tree. Most likely you're using an unsupported version of androidx.compose.ui:ui. The supported @@ -152,7 +164,7 @@ internal object ComposeViewHierarchyNode { // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface // as crashes instead of silently degrading masking if (SentryReplayDebug.failFast) { - throw t + throw semanticsFailure } // If we're unable to retrieve the semantics configuration @@ -284,13 +296,23 @@ internal object ComposeViewHierarchyNode { return false } + var traversalFailure: Throwable? = null try { val rootNode = (view as? Owner)?.root ?: return false rootNode.traverse(parent, isComposeRoot = true, options, logger) + } catch (e: LinkageError) { + // an unsupported Compose version, which must not crash the app + traversalFailure = e } catch (e: Throwable) { + // the traversal runs app code, so we can't know what it throws + ExceptionUtils.rethrowIfFatal(e) + traversalFailure = e + } + + if (traversalFailure != null) { logger.log( SentryLevel.ERROR, - e, + traversalFailure, """ Error traversing Compose tree. Most likely you're using an unsupported version of androidx.compose.ui:ui. The minimum supported version is 1.5.0. If it's a newer @@ -302,7 +324,7 @@ internal object ComposeViewHierarchyNode { // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface // as crashes instead of silently skipping the whole Compose subtree (i.e. not masking it) if (SentryReplayDebug.failFast) { - throw e + throw traversalFailure } return false } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt index 10d02103ba1..39325bda05e 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeHelper.kt @@ -35,8 +35,10 @@ internal class SentryComposeHelper(logger: ILogger) { val value = sentryTagElementField.get(modifier) return value as String? } - } catch (e: Throwable) { - // ignored + } catch (_: IllegalAccessException) { + // fall back to SemanticsModifier below + } catch (_: ClassCastException) { + // the tag is not a String in this Compose version } // Older versions use SemanticsModifier diff --git a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt index 6632efce93f..339e12c052c 100644 --- a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt +++ b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt @@ -17,8 +17,10 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.CookieUtils +import io.sentry.util.ExceptionUtils import io.sentry.util.HttpUtils import io.sentry.util.UrlUtils +import kotlin.coroutines.cancellation.CancellationException internal object SentryKtorClientUtils { internal suspend fun captureClientError( @@ -51,7 +53,12 @@ internal object SentryKtorClientUtils { statusCode = response.status.value try { bodySize = response.bodyAsBytes().size.toLong() - } catch (_: Throwable) {} + } catch (e: CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Throwable) { + // The HTTP engine can throw anything while reading the body. The body size is optional. + ExceptionUtils.rethrowIfFatal(e) + } } event.request = sentryRequest diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java index 16599595866..45dd6026874 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java @@ -2,6 +2,7 @@ import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.ContextStorageProvider; +import io.sentry.util.ExceptionUtils; import java.util.Iterator; import java.util.ServiceLoader; import org.jetbrains.annotations.NotNull; @@ -23,8 +24,11 @@ public ContextStorage get() { return contextStorageProvider.get(); } } + } catch (LinkageError ignored) { + // a provider needs a class that is missing or has an incompatible version, use fallback } catch (Throwable t) { - // ignore and use fallback + // Other providers are third-party code, so we can't know what they throw. Use fallback. + ExceptionUtils.rethrowIfFatal(t); } // using default / fallback storage diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 932569177b4..f83f6667f6d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -21,6 +21,7 @@ import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.DebugMetaPropertiesApplier; +import io.sentry.util.ExceptionUtils; import io.sentry.util.FileUtils; import io.sentry.util.InitUtil; import io.sentry.util.LoadClass; @@ -362,7 +363,21 @@ private static void init(final @NotNull SentryOptions options, final boolean glo for (final Integration integration : options.getIntegrations()) { try { integration.register(ScopesAdapter.getInstance(), options); + } catch (LinkageError e) { + // The integration needs an optional dependency that is missing or has an incompatible + // version. It is skipped and init continues. + options + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to register the integration " + + integration.getClass().getName() + + ", a dependency it needs is missing or incompatible", + e); } catch (Throwable t) { + // Integrations can be user-provided, so we can't know what they throw. A failing + // integration is skipped and init continues. + ExceptionUtils.rethrowIfFatal(t); options .getLogger() .log( diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 71dd4588e33..c9f143df377 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -737,8 +737,8 @@ public void activate() { try { final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); integrations.add((Integration) clazz.getConstructor().newInstance()); - } catch (Throwable ignored) { - // SpotlightIntegration not available + } catch (ReflectiveOperationException | LinkageError ignored) { + // sentry-spotlight is missing or has an incompatible version } } } diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 07e9b6357d7..6be630a0c7a 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -144,6 +144,9 @@ public void uncaughtException(Thread thread, Throwable thrown) { } } } catch (Throwable e) { + // Deliberately broad and without ExceptionUtils.rethrowIfFatal: this thread is already + // dying, and rethrowing would skip the chained handler below, which is what makes the app + // crash as it normally would (on Android, it kills the process). The crash event is lost. options .getLogger() .log(SentryLevel.ERROR, "Error sending uncaught exception to Sentry.", e); diff --git a/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java b/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java index 137f1cd9156..c93d19ebf75 100644 --- a/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java +++ b/sentry/src/main/java/io/sentry/cache/tape/QueueFile.java @@ -379,7 +379,7 @@ boolean ringRead(long position, byte[] buffer, int offset, int count) throws IOE resetFile(); } catch (IOException e) { throw e; - } catch (Throwable e) { + } catch (IndexOutOfBoundsException e) { // most likely the file is corrupt, so we delete it and recreate, accepting data loss resetFile(); } diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index cf86a13fc53..683eea8a866 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -9,6 +9,7 @@ import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import java.util.Iterator; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -38,7 +39,8 @@ public final class ProfilingServiceLoader { logger.log( SentryLevel.DEBUG, "No continuous profiler provider found, using NoOpContinuousProfiler"); return NoOpContinuousProfiler.getInstance(); - } catch (Throwable t) { + } catch (ServiceConfigurationError | LinkageError t) { + // The provider is broken, or async-profiler's native library failed to load logger.log( SentryLevel.ERROR, "Failed to load continuous profiler provider, using NoOpContinuousProfiler", @@ -68,7 +70,8 @@ public final class ProfilingServiceLoader { SentryLevel.DEBUG, "No profile converter provider found, using NoOpProfileConverter"); return NoOpProfileConverter.getInstance(); } - } catch (Throwable t) { + } catch (ServiceConfigurationError | LinkageError t) { + // The provider is broken, or a class it needs is missing logger.log( SentryLevel.ERROR, "Failed to load profile converter provider, using NoOpProfileConverter", diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java index bf179fad459..aedd27a14be 100644 --- a/sentry/src/main/java/io/sentry/util/LoadClass.java +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -41,7 +41,8 @@ public class LoadClass { if (logger != null) { logger.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); } - } catch (Throwable e) { + } catch (LinkageError e) { + // e.g. ExceptionInInitializerError, or NoClassDefFoundError for a dependency of the class if (logger != null) { logger.log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 8b87e67f9f5..8d0e2c54e0d 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -57,6 +57,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.check +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock @@ -680,6 +681,49 @@ class SentryTest { verify(logger).log(eq(SentryLevel.ERROR), any(), eq(initException)) } + @Test + fun `init skips an integration whose dependency is missing and registers the rest`() { + val failing = mock() + doThrow(NoClassDefFoundError("missing")).whenever(failing).register(any(), any()) + val next = mock() + + initForTest { + it.dsn = dsn + it.addIntegration(failing) + it.addIntegration(next) + } + + verify(next).register(any(), any()) + } + + @Test + fun `init skips an integration that throws on register and registers the rest`() { + val failing = mock() + doThrow(IllegalStateException("register")).whenever(failing).register(any(), any()) + val next = mock() + + initForTest { + it.dsn = dsn + it.addIntegration(failing) + it.addIntegration(next) + } + + verify(next).register(any(), any()) + } + + @Test + fun `init rethrows a fatal error from an integration's register`() { + val failing = mock() + doThrow(OutOfMemoryError("register")).whenever(failing).register(any(), any()) + + assertThrows(OutOfMemoryError::class.java) { + initForTest { + it.dsn = dsn + it.addIntegration(failing) + } + } + } + @Test fun `overrides envelope cache if it's not set`() { var sentryOptions: SentryOptions? = null diff --git a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt index 4cb921feff8..3dcc478bb9f 100644 --- a/sentry/src/test/java/io/sentry/util/LoadClassTest.kt +++ b/sentry/src/test/java/io/sentry/util/LoadClassTest.kt @@ -74,6 +74,17 @@ class LoadClassTest { assertTrue(LoadClassInitFlag.initialized) } + + @Test + fun `loadClass returns null when the static initializer of the class throws`() { + assertNull(LoadClass().loadClass(LoadClassFailingInitProbe::class.java.name, null)) + } +} + +private object LoadClassFailingInitProbe { + init { + throw IllegalStateException("init") + } } private object IsClassAvailableNoInitFlag { From f92375f3751d5c0912df8861c1e9178aa40ed7b5 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 11 Nov 2026 15:32:56 +0100 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d0cec6fc1..c5193dc883f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,9 @@ - On a busy device during cold start this could stall the launcher activity's `onCreate` for hundreds of milliseconds. - Do not inspect the response body of streaming Apollo 3 and 4 responses (`multipart/mixed`, `text/event-stream`) for failed requests, which blocked `@defer` and `@stream` ([#6432](https://github.com/getsentry/sentry-java/pull/6432)) - Set Apollo 3, 4, and 5 HTTP spans to `CANCELLED` instead of `INTERNAL_ERROR` when the request is cancelled ([#6284](https://github.com/getsentry/sentry-java/pull/6284)) +- Stop swallowing fatal errors such as `OutOfMemoryError` in the SDK code that loads native libraries and optional dependencies ([#6562](https://github.com/getsentry/sentry-java/pull/6562)) + - The offline cache no longer deletes its queue file when it runs out of memory while reading it. + - The Ktor client integration no longer swallows coroutine cancellation while reading the response body of a failed request. ### Improvements