Skip to content

Don't hijack the global uncaught exception handler during Docker probing#11851

Open
seonwooj0810 wants to merge 1 commit into
testcontainers:mainfrom
seonwooj0810:fix/issue-11483-dont-catch-uncaught-exceptions
Open

Don't hijack the global uncaught exception handler during Docker probing#11851
seonwooj0810 wants to merge 1 commit into
testcontainers:mainfrom
seonwooj0810:fix/issue-11483-dont-catch-uncaught-exceptions

Conversation

@seonwooj0810

Copy link
Copy Markdown

Fixes #11483

Problem

DockerClientProviderStrategy.test() probes the Docker socket through an executor-backed Awaitility.await()...untilAsserted(...). Awaitility's default has uncaught-exception catching enabled, and in that mode ConditionAwaiter installs its own Thread.UncaughtExceptionHandler as the JVM-global default handler for the duration of the await:

// org.awaitility.core.ConditionAwaiter (4.3.0)
OriginalDefaultUncaughtExceptionHandler.set(Thread.getDefaultUncaughtExceptionHandler());
if (conditionSettings.shouldCatchUncaughtExceptions()) {
    Thread.setDefaultUncaughtExceptionHandler(this);
}

So while Testcontainers initializes, the application's global uncaught exception handler is replaced and exceptions can be intercepted by Awaitility instead of the application's handler — exactly the stack trace in #11483:

at java.lang.Thread.setDefaultUncaughtExceptionHandler(Thread.java:2496)
at org.testcontainers.shaded.org.awaitility.core.ConditionAwaiter.<init>(ConditionAwaiter.java:59)
...
at org.testcontainers.dockerclient.DockerClientProviderStrategy.test(DockerClientProviderStrategy.java:214)

Fix

The probed condition (socket.connect(...)) runs on a single dedicated poll thread and never relies on exceptions surfacing from other threads, so Awaitility's uncaught-exception catching provides no value here. Opt out per-call with dontCatchUncaughtExceptions() so the application's global handler is left untouched.

This is scoped to the one affected call site. I verified that the other Awaitility.await() usages in core (GenericContainer, HostPortWaitStrategy, RemoteDockerImage) all use pollInSameThread(), and same-thread polling does not install the global handler — so they are unaffected (confirmed by the test below). A per-call opt-out is preferred over the global Awaitility.doNotCatchUncaughtExceptionsByDefault() switch, which would change Awaitility's behaviour for user code too.

Test evidence

Added AwaitilityUncaughtExceptionHandlerTest, which drives the same executor-backed await test() uses and observes the active default handler while polling:

  • defaultExecutorPollingHijacksTheGlobalHandler — documents the broken behaviour (handler is replaced).
  • dontCatchUncaughtExceptionsKeepsTheGlobalHandler — with the fix the application handler stays in place.

Both pass; the first fails if dontCatchUncaughtExceptions() is removed.

Verification done

  1. No in-flight PR (searched open PRs for UncaughtExceptionHandler).
  2. No assignee / no self-claim on the issue.
  3. Code-only change + regression test.
  4. Confirmed against current main: decompiled the shaded Awaitility 4.3.0 ConditionAwaiter to verify the handler install is gated on shouldCatchUncaughtExceptions(), and demonstrated via the test that executor polling installs it while same-thread polling does not.
  5. ./gradlew :testcontainers:test --tests "...AwaitilityUncaughtExceptionHandlerTest" and :testcontainers:spotlessApply pass.

DockerClientProviderStrategy.test() probes the Docker socket through an
executor-backed Awaitility await. With Awaitility's default settings,
uncaught-exception catching is enabled, which installs Awaitility's own
Thread.UncaughtExceptionHandler as the JVM-global default handler for the
duration of the await. This silently replaces the application's handler
while Testcontainers initializes (see the stack trace in testcontainers#11483).

The probed condition runs on a single dedicated thread and never relies
on exceptions propagating from other threads, so catching uncaught
exceptions provides no value here. Opt out with dontCatchUncaughtExceptions()
so the application's global handler is left untouched.

Fixes testcontainers#11483
@seonwooj0810 seonwooj0810 requested a review from a team as a code owner June 19, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Running testcontainers temporarily changes global uncaught exception handler

1 participant