Repository navigation
Fix Web Service Listener SOAP requests failing on JDK 19+ - #508
jonbartels wants to merge 2 commits into
Conversation
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>
tonygermano
left a comment
There was a problem hiding this comment.
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-derbyon 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/javaxthe wayMirthLauncherdoes 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 | |||
There was a problem hiding this comment.
| // 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.
There was a problem hiding this comment.
This file needs a SPDX header. See note on WebServiceListenerTest.java.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
|
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.
That covers the MTOM/attachments item under "Not tested", and Java 25, which CI doesn't run. |
What
WebServiceReceivernow builds its HttpServer worker pool with aThreadFactorythat sets each worker's context classloader to the one that loadedWebServiceReceiver.WebServiceReceiverTest: starts a realHttpServeron 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, whileGET ?wsdlkeeps 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/aworkaround 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'ssetContextClassLoaderline removed,WebServiceReceiverTestfails (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),WebServiceListenerTestfails with HTTP 500 and the other 12 pass.🤖 Generated with Claude Code