Skip to content

Fix Web Service Listener SOAP requests failing on JDK 19+ - #508

Draft
jonbartels wants to merge 2 commits into
OpenIntegrationEngine:mainfrom
jonbartels:fix/ws-listener-jdk19-classloader
Draft

jonbartels wants to merge 2 commits into
OpenIntegrationEngine:mainfrom
jonbartels:fix/ws-listener-jdk19-classloader

Conversation

@jonbartels

Copy link
Copy Markdown
Contributor

What

  • WebServiceReceiver now builds its HttpServer worker pool with a ThreadFactory that sets each worker's context classloader to the one that loaded WebServiceReceiver.
  • WebServiceReceiverTest: starts a real HttpServer on that pool and checks the request thread carries the given classloader.
  • WebServiceListenerTest (smoke): deploys a Web Service Listener on port 8081 and POSTs a SOAP envelope to it from the harness container.

Why

On JDK 19 and later, every SOAP request to a Web Service Listener fails with Unable to create SAAJ meta-factory, while GET ?wsdl keeps returning 200.

The JDK HttpServer creates worker threads from its dispatcher thread. Since JDK 19 that thread gets the system classloader instead of inheriting the caller's, so the workers can't see server-lib, and SAAJ's context-classloader lookup misses the implementation. On JDK 18 and earlier the workers inherited the server's classloader, which this restores. No -Xbootclasspath/a workaround is needed.

This was reported against the BridgeLink fork, where it was root-caused and fixed in a similar way: Innovar-Healthcare/BridgeLink#205. Their fix pins the channel's resource classloader on the workers. This PR uses the server classloader instead, which keeps the behaviour from before JDK 19 and doesn't make channel libraries visible to the SOAP stack.

The fixture tests can't catch this, because they submit messages through the client API and never touch the listener's worker threads. That's why the smoke test is a Java test.

Testing

  • ./gradlew :server:test --tests 'com.mirth.connect.connectors.ws.*' passes. With the fix's setContextClassLoader line removed, WebServiceReceiverTest fails (expected URLClassLoader, was AppClassLoader).
  • ci/runtests.sh alpine-temurin21-derby: 13/13 pass. With the fix line removed (and unit tests skipped in the image build), WebServiceListenerTest fails with HTTP 500 and the other 12 pass.
  • Not tested: MTOM/attachment requests, and the other CI configurations locally.

🤖 Generated with Claude Code

jonbartels and others added 2 commits October 23, 2026 08:26
On JDK 19 and later every SOAP request to a Web Service Listener fails
with "Unable to create SAAJ meta-factory". The HttpServer creates its
worker threads from its dispatcher thread, which since JDK 19 gets the
system classloader instead of inheriting one. The workers inherit that,
cannot see server-lib, and SAAJ's context-classloader lookup misses the
implementation. GET ?wsdl keeps working because it never touches SAAJ.

Build the worker pool with a ThreadFactory that sets the classloader
that loaded WebServiceReceiver, which is what the workers inherited on
JDK 18 and earlier. No -Xbootclasspath workaround is needed.

Reported against the BridgeLink fork:
Innovar-Healthcare/BridgeLink#205

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jon Bartels <jonathan.bartels@gmail.com>
Fixture tests submit messages through the client API, which never
reaches the listener's HTTP worker threads, so they could not catch the
JDK 19 SAAJ failure. This Java test deploys a Web Service Listener on
port 8081 and POSTs a SOAP envelope to it from the harness container.
Without the previous commit it fails with HTTP 500 on Temurin 21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jon Bartels <jonathan.bartels@gmail.com>
@github-actions

Copy link
Copy Markdown

Test Results

130 files  +1  130 suites  +1   3m 8s ⏱️ +44s
739 tests +2  739 ✅ +2  0 💤 ±0  0 ❌ ±0 
835 runs  +8  829 ✅ +8  6 💤 ±0  0 ❌ ±0 

Results for commit 98e6b66. ± Comparison against base commit 6d348a0.

@tonygermano tonygermano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is the fix I was hoping for. It matches the root cause in #509, and the classloader choice is better than what I proposed there.

Classloader choice

In #509 I suggested capturing the context classloader of the thread that calls onStart(). Using WebServiceReceiver.class.getClassLoader() is better because it's deterministic. In production that's the URLClassLoader that MirthLauncher builds from server-lib and the extensions (MirthLauncher.java:110-111), which is the loader the worker threads inherited before JDK 19, so it restores the old behavior without depending on whatever thread happens to start the connector. I agree with not using the channel's resource classloader: custom web service classes don't need it at request time, because they're loaded through it and JAX-WS builds its model during publish(), and with a child-first resource that bundles its own SAAJ it could cause class conflicts.

Testing

Claude checked:

  • ./gradlew :server:test --tests 'com.mirth.connect.connectors.ws.*' passes on 98e6b66.
  • ci/runtests.sh alpine-temurin21-derby on 98e6b66: 13/13 pass, including "Web Service Listener: accepts a SOAP request".
  • On the "not tested: MTOM/attachment requests" item: a standalone harness that loads server-lib/javax the way MirthLauncher does showed plain SOAP, MTOM and SOAP-with-Attachments requests all return 200 when the worker threads have the launcher classloader (JDK 17 and 18), and all return 500 on JDK 19 and 21 without it. The state this PR restores is the one that works for all three.

The unit test fails without the fix on any JDK, not just 19+, because the worker would otherwise get the test thread's classloader. That's a nice property.

I left a few chore tasks inline, but otherwise, this is fantastic. Thanks!


🤖 Drafted with Claude Code

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Open Integration Engine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SPDX-FileCopyrightText: Open Integration Engine
// SPDX-FileCopyrightText: Open Integration Engine Contributors

This is what you should use for a generic copyright text. The human authors hold the copyright, not the project. You can also use your own name for a new file if you choose to.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file needs a SPDX header. See note on WebServiceListenerTest.java‎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please convert the header on this file to SPDX? If this was a derivative work (i.e., you told your AI to consider the BridgeLink implementation before suggesting changes) then make sure to include a copyright line after Mirth from the source file.

// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Mirth Corporation
// SPDX-FileCopyrightText: Open Integration Engine Contributors <or Jon Bartels>


@Test
public void testWorkerThreadsUseGivenContextClassLoader() throws Exception {
ClassLoader expected = new URLClassLoader(new URL[0], getClass().getClassLoader());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please close the URLClassLoader, for example with try-with-resources.

* see server-lib, and lookups through the context classloader (e.g. the SAAJ implementation)
* fail. Give the worker threads the given classloader explicitly instead.
*/
static ExecutorService createExecutor(int threads, ClassLoader contextClassLoader) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Import and annotate this method with com.google.common.annotations.VisibleForTesting.

This is from Guava. It's already on the classpath and also used in JavaScriptBuilder. It lets static analysis tools know that this method should be treated as private except for test class access.

@pacmano1

Copy link
Copy Markdown
Contributor

Ran it on a real engine: container builds of main and 98e6b66, Java 17, 21 and 25, with this PR's smoke channel, sending plain SOAP, MTOM and SOAP-with-Attachments POSTs.

  • main: 200 on 17; 500 on 21 and 25, with Unable to create SAAJ meta-factory.
  • This PR: 200 on all three Java versions, for all three request types.

That covers the MTOM/attachments item under "Not tested", and Java 25, which CI doesn't run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Web Service Listener fails every SOAP request on Java 19+ (Unable to create SAAJ meta-factory)

3 participants