Skip to content

Don't replace the global uncaught exception handler during container startup#11920

Open
Develop-KIM wants to merge 1 commit into
testcontainers:mainfrom
Develop-KIM:fix-awaitility-global-uncaught-handler
Open

Don't replace the global uncaught exception handler during container startup#11920
Develop-KIM wants to merge 1 commit into
testcontainers:mainfrom
Develop-KIM:fix-awaitility-global-uncaught-handler

Conversation

@Develop-KIM

Copy link
Copy Markdown

Fixes #11483

Problem

Testcontainers uses (shaded) Awaitility in a few core paths that run on every container startup:

  • DockerClientProviderStrategy#test – Docker client discovery
  • RemoteDockerImage – image pull
  • HostPortWaitStrategy – host-port wait

By default Awaitility installs a global default uncaught exception handler via Thread.setDefaultUncaughtExceptionHandler(...) for the duration of each await() and restores the previous one afterwards. So just starting a container temporarily replaces the caller's default handler, and during that window uncaught exceptions thrown by unrelated application threads are intercepted by Awaitility instead of reaching the application's handler.

The stack trace in the issue is exactly this path:

Thread.setDefaultUncaughtExceptionHandler
  ConditionAwaiter.<init>
  ...
  DockerClientProviderStrategy.test
  DockerClientProviderStrategy.getFirstValidStrategy
  DockerClientFactory.client
  GenericContainer.start

Fix

Add .dontCatchUncaughtExceptions() to the three core Awaitility.await() chains, so Testcontainers no longer mutates this JVM-global state. The awaited conditions run in the awaiting / same thread (pollInSameThread), so there is no separate thread whose uncaught exceptions Awaitility would need to catch here.

Verification

  • Added a regression test (DockerClientProviderStrategyUncaughtExceptionHandlerTest) that installs a sentinel default handler, drives DockerClientProviderStrategy#test against a closed port, and asserts the global handler is never replaced. Because Awaitility restores the handler once the await finishes, the check has to observe during the await, so it uses a small watcher thread. The test fails on main and passes with this change.
  • Also verified end-to-end against a real container (started with a delayed port listener so the wait strategy polls for a few seconds): the global handler is replaced during startup on main, and left untouched with this change.

This change was drafted with AI assistance (Claude); I reviewed and verified it end-to-end and can speak to every line.

…startup

Testcontainers uses Awaitility in a few core code paths (Docker client
discovery, image pulling and the host-port wait strategy). By default
Awaitility installs a global default uncaught exception handler through
Thread.setDefaultUncaughtExceptionHandler for the duration of each await
and restores it afterwards. As a result, simply starting a container
temporarily replaces the caller's default handler and can intercept
uncaught exceptions thrown by unrelated threads during that window.

Opt out of that behaviour with dontCatchUncaughtExceptions() so
Testcontainers no longer mutates this JVM-global state.

See testcontainers#11483

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Develop-KIM Develop-KIM requested a review from a team as a code owner July 9, 2026 16:23
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