Skip to content

Communicator.sendCall() hard-references javax.xml.soap.SOAPMessage on the JSON path — NoClassDefFoundError on JDK 11+ / OSGi kills the WebSocket listener on the first charge-point session #448

Description

@newman85366-commits

Affected versions: v1.1 AND v1.2. Fixed in v2.0 (released
2025-12-12), where Communicator.java carries no SOAP reference at all.

Verified by reading Communicator.java at each tag on 2026-08-20 — not inferred
from release notes:

tag import javax.xml.soap.SOAPMessage instanceof SOAPMessage in sendCall()
v1.1 present present (line 157)
v1.2 present present
v2.0 absent absent — sendCall() rewritten around the Radio interface

Also present on the v1.1 tag in ocpp-common/.../utilities/SugarUtil.java.

Correction to an earlier draft of this report. We first wrote that the
branch was gone "on master (d098fc6)" and asked for a patch off v1.1. That
understated the position twice: the fix is in a released version, not only
on master, and the defect also survives in v1.2, which our first pass never
checked. Both claims are corrected above, and the ask below changed as a
result.

Environment where we hit it: OpenEMS Edge 2026.8.0 (which pins this library
via io.openems.wrapper/eu.chargetime.ocpp.bnd), Apache Felix, Eclipse
Temurin 21, Docker. OCPP-J 1.6 only — no SOAP transport configured or wanted.

Summary

Communicator.sendCall() executes an instanceof SOAPMessage test on every
outgoing call
, on both the JSON and the SOAP transports:

// ocpp-common/src/main/java/eu/chargetime/ocpp/Communicator.java
 32:  import javax.xml.soap.SOAPMessage;
...
153:  public synchronized void sendCall(String uniqueId, String action, Request request) {
154:    Object call = makeCall(uniqueId, action, packPayload(request));
155:
156:    if (call != null) {
157:      if (call instanceof SOAPMessage) {
158:        logger.trace("Send a message: {}", SugarUtil.soapMessageToString((SOAPMessage) call));
159:      } else {
160:        logger.trace("Send a message: {}", call);
161:      }
162:    }

javax.xml.soap (SAAJ) was deprecated in Java 9 and removed from the JDK in
Java 11
(JEP 320). ocpp-common/pom.xml:55-58 declares
javax.xml.soap:javax.xml.soap-api:1.4.0 at compile scope, so a plain Maven
build still works. Under OSGi, or under any deployment that does not put SAAJ on
the runtime classpath, resolving the instanceof throws.

Two things make this much worse than a missing trace log:

  1. The branch is on the JSON path. A pure OCPP-J deployment that never
    touches SOAP still evaluates it, because instanceof requires the JVM to
    resolve SOAPMessage regardless of the runtime type of call. logger.trace
    being disabled does not help — the branch is evaluated before the log call.
  2. It is thrown inside the WebSocket handler thread. The resulting
    NoClassDefFoundError propagates out of the java-websocket callback, the
    listener dies, and port 8887 stops accepting. So the failure is not "one
    dropped message" — the first charge point to send anything takes the whole
    server down for every charge point.

Suggested fixes, cheapest first

  1. Move the SOAP-specific logging into the SOAP subclass. SOAPCommunicator
    already exists in ocpp-v1_6; ocpp-common should not name a SOAP type at
    all. This is what master effectively did.
  2. If the branch must stay in ocpp-common, gate it without loading the class:
    if ("javax.xml.soap.SOAPMessage".equals(call.getClass().getName())), or
    Class.forName guarded once and cached, or reflection in SugarUtil.
  3. At minimum, wrap the trace block in try { … } catch (NoClassDefFoundError | LinkageError e) { logger.trace("Send a message: {}", call); } so a logging
    concern cannot kill a transport.
  4. If SAAJ genuinely remains required at runtime, say so in the README and move
    the dependency out of provided/compile-only assumptions.

What we are actually asking for

v2.0 already fixes this, so this is not a request for a fix — it is a question
about the 1.x line.
Either answer is useful and we will act on it:

  • (a) A 1.2.x patch release carrying only the removal of this branch, for
    consumers pinned to 1.x; or
  • (b) A statement that 2.0 is the only supported path, in which case
    please say so plainly — downstreams pinned at 1.1 (OpenEMS among them) need to
    know the 1.x line will not receive it, so they can plan the major bump.

We are not asking you to backport the rest of master. If (b), we will carry the
request downstream ourselves — see "Why this still matters to OpenEMS" below.

Related: this was predicted here in 2019

#94 — "Refactor downcast of message types in higher level components"
(closed, enhancement + good first issue) named this exact anti-pattern and
its reason: the downcast of message types couples higher-level components to the
XML/SOAP libraries, and SOAP support had by then moved into the ocpp-v1_6
subproject. This report is that observation arriving as a production failure
seven years later — the JSON path still executing a SOAP downcast, on a JDK
where the class no longer ships.

Not a duplicate: #94 is a design-cleanliness enhancement with no crash, no
NoClassDefFoundError, and no version boundary. It is the argument for why the
1.x line still deserves the removal.

Also checked and not duplicates: #19 (SOAPSyncHelper.java issue — a
forwardMessage()/promises ordering bug in the SOAP transport) and #137
(NoClassDefFoundError: eu/chargetime/ocpp/wss/WssSocketBuilder — a user who
omitted OCPP-J-*.jar from their classpath, self-resolved). Same exception type
in #137, entirely different cause: a library class missing because a jar was
left out, versus a JDK class the library references but does not declare.

Why this still matters to OpenEMS — verified on develop, 2026-08-20

This is not only a problem for our pinned tag. On OpenEMS's current default
branch
:

# io.openems.wrapper/eu.chargetime.ocpp.bnd  (develop, 2026-08-20)
Bundle-Version: 1.1.0
Include-Resource: @v1_6-1.1.0.jar, @OCPP-J-1.0.2.jar, @common-1.0.2.jar
Import-Package: com.sun.activation.registries, com.google.gson,
    javax.xml.transform, org.java_websocket, ..., org.w3c.dom
#   ^ javax.xml.soap is ABSENT

So the pin is still 1.1.0 and the import is still missing — the defect is live
on develop, not merely on the release we happened to build.

And OpenEMS can fix its half in one line. io.openems.edge.application/
EdgeApp.bndrun already uses -runproperties, for a different property:

-runproperties:\
	org.osgi.service.http.port=8080,\
	felix.cm.dir=c:/openems/config,\
	openems.data.dir=c:/openems/data,\
	org.apache.felix.eventadmin.Timeout=0,\
	org.ops4j.pax.logging.DefaultServiceLog.level=WARN

Adding org.osgi.framework.bootdelegation=javax.xml.soap to that list is the
whole of the framework-side fix, in a file where the mechanism is already in
use — no new machinery, no new build step. (The SAAJ jar must still be supplied;
see the workaround below for why both halves are needed.) We will raise this
separately on the OpenEMS tracker and cross-link it here.

Workaround we ship (for anyone else hitting this)

Two halves, and both are required. Supplying the class is not enough on
OSGi, because the wrapper bundle's explicit Import-Package does not list
javax.xml.soap, so normal OSGi resolution will never wire it:

# io.openems.wrapper/eu.chargetime.ocpp.bnd — Import-Package, verbatim at 2026.8.0
Import-Package: \
    com.sun.activation.registries,\
    com.google.gson,\
    javax.xml.transform,\
    org.java_websocket,\
    ...
    org.w3c.dom,\
#   ^ no javax.xml.soap
  1. Supply the class. Add javax.xml.soap-api-1.4.0.jar to the image and put
    it on the boot classloader via -Xbootclasspath/a: — that is what Felix's
    default org.osgi.framework.bundle.parent=boot delegates to.
  2. Permit the delegation. Set
    org.osgi.framework.bootdelegation=javax.xml.soap, which bypasses
    Import-Package resolution entirely.

Trap, and the reason this is worth writing down. The delegation must go
into -runproperties in the .bnd at build time. Passing
-Dorg.osgi.framework.bootdelegation=javax.xml.soap on the java command line
is silently ignored — no warning, the identical stack comes straight back.
bnd's biz.aQute.launcher.Launcher.createFramework() builds the framework
config as Properties p = new Properties(); p.putAll(properties) from the
launcher.properties baked into the executable jar, and never copies
System.getProperties(). -Dfelix.cm.dir does work, because that is a
bundle property read through BundleContext.getProperty, which falls back to
System.getProperty. Framework launching properties are a different read
path. Verify the fix landed with:
unzip -p openems-edge.jar launcher.properties | grep bootdelegation
— which is checkable without even starting the edge.

We are happy to supply logs, our reproduction, or to test a patch.

##Operating System
Docker on Windows 11, Eclipse Temurin 21, Apache Felix

Reproduction

  1. Run JSONServer on a JDK ≥ 11 with SAAJ absent from the runtime classpath
    (an OSGi container, or plain java -cp without javax.xml.soap-api).
  2. Connect any OCPP-J 1.6 charge point and let the server send one request
    (ChangeAvailability from an equivalent of OpenEMS's sendInitialRequests
    is enough).
  3. Observe NoClassDefFoundError: javax/xml/soap/SOAPMessage and, immediately
    after, that TCP connects to the listener port are refused.

Expected: a JSON-only deployment never needs SAAJ, and a failure in a trace
log path never takes down the listener.

Observed: first session kills the server.


Downstream Impact / Cross-reference:
This issue was identified during integration with OpenEMS: OpenEMS/openems#3896


Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions