Skip to content

SOAP Handling

Philip Helger edited this page Sep 4, 2026 · 2 revisions

This page describes how phase4 deals with SOAP: the supported SOAP versions, the processing of incoming SOAP header elements and - most importantly - the handling of received SOAP Faults.

AS4 is built on top of SOAP, so every AS4 message is a SOAP message:

  • the ebMS eb:Messaging header element and the WS-Security wsse:Security header element reside in the SOAP Header
  • the payload resides either in the SOAP Body or - when using "SOAP with Attachments" - in MIME body parts (the latter is e.g. required by the Peppol, the HR eDelivery and the eDelivery AS4 2.0 profile)

SOAP versions

All supported SOAP versions are contained in the enum ESoapVersion (in package com.helger.phase4.model):

Enum constant Version Namespace URI Namespace prefix MIME type mustUnderstand "true" value
SOAP_11 1.1 http://schemas.xmlsoap.org/soap/envelope/ S11 text/xml 1
SOAP_12 1.2 http://www.w3.org/2003/05/soap-envelope S12 application/soap+xml true

The constant ESoapVersion.AS4_DEFAULT is SOAP_12, because the AS4 specification (chapter 2.1 "Feature Set") mandates SOAP 1.2. SOAP 1.1 is supported for interoperability with non-conforming peers only.

The following lookup methods are available:

  • ESoapVersion.getFromVersionOrNull(String) and getFromVersionOrDefault(String, ESoapVersion) - based on the version string (1.1 or 1.2)
  • ESoapVersion.getFromNamespaceURIOrNull(String) - based on the XML namespace URI
  • ESoapVersion.getFromMimeTypeOrNull(IMimeType) - based on the MIME type

Additionally the enum provides getMustUnderstandValue(boolean), getHeaderElementName() (always Header), getBodyElementName() (always Body) and getMimeType(Charset).

Outgoing messages

The SOAP version to be used for sending is ESoapVersion.AS4_DEFAULT by default. It can be changed via

  • AbstractAS4MessageBuilder.soapVersion(ESoapVersion) on the message builders, or
  • AbstractAS4Client.setSoapVersion(ESoapVersion) on the low-level clients

The SOAP version is also part of the PMode, in PModeLegProtocol (see getSoapVersion() / setSoapVersion(...)).

Marshalling of the SOAP envelope is done via the classes Soap11EnvelopeMarshaller and Soap12EnvelopeMarshaller (package com.helger.phase4.marshaller), that work on the JAXB generated classes in the packages com.helger.phase4.soap11 and com.helger.phase4.soap12. For "SOAP with Attachments" the class AS4SoapMimeMultipart (package com.helger.phase4.messaging.mime) creates a multipart/related MIME message and adds the type parameter with the MIME type of the used SOAP version to the Content-Type header - as required by RFC 2387. No charset parameter is added to the multipart Content-Type (see #263).

Incoming messages

For incoming messages the SOAP version is determined dynamically:

  • For multipart messages: first from the Content-Type of the first MIME part, and if that fails, from the namespace URI of the XML root element
  • For non-multipart messages: first from the namespace URI of the XML root element, and if that fails, from the Content-Type of the request

If the SOAP version cannot be determined, the message is not processed any further and no message processing callback is invoked. The problem is logged on error level - or on warning level only, if this is the response to an outgoing message that used a non-success HTTP status code, because such a body is most likely an infrastructure error page rather than an AS4 protocol violation (see #378).

Incoming SOAP header processing

Incoming SOAP header elements are processed by implementations of the interface ISoapHeaderElementProcessor (package com.helger.phase4.incoming.soap). Each processor is registered for exactly one QName in a SoapHeaderElementProcessorRegistry.

The default registry is created via SoapHeaderElementProcessorRegistry.createDefault(...) and contains the following processors - the registration order is the execution order:

Order Header element QName Processor Purpose
1 {http://docs.oasis-open.org/ebxml-msg/ebms/v3.0/ns/core/200704/}Messaging SoapHeaderElementProcessorExtractEbms3Messaging Extracts the ebMS 3.0 Messaging element, resolves the PMode etc.
2 {http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}Security SoapHeaderElementProcessorWSS4J Signature verification and decryption via WSS4J

The respective QName constants are SoapHeaderElementProcessorExtractEbms3Messaging.QNAME_MESSAGING and SoapHeaderElementProcessorWSS4J.QNAME_SECURITY.

Each SOAP header element of an incoming message is represented by an AS4SingleSoapHeader instance, which additionally remembers whether the element carries mustUnderstand="true" (the version specific value - see ESoapVersion.getMustUnderstandValue(boolean)) and whether it was processed.

After all registered processors were invoked, the SOAP "must understand" requirement is enforced: if a header element with mustUnderstand="true" was not processed, the message is rejected with a Phase4IncomingException that is marked as "retry not feasible" and that uses the HTTP status code CAS4Soap.HTTP_STATUS_CODE_MUST_UNDERSTAND.

The class CAS4Soap (since v4.1.1) contains the SOAP Fault to HTTP status code mapping of https://www.w3.org/TR/soap12-part2/#soapinhttp chapter 7.5.2.2, "Table 20":

Constant HTTP status code
HTTP_STATUS_CODE_VERSION_MISMATCH 500
HTTP_STATUS_CODE_MUST_UNDERSTAND 500
HTTP_STATUS_CODE_SENDER 400
HTTP_STATUS_CODE_RECEIVER 500
HTTP_STATUS_CODE_DATA_ENCODING_UNKNOWN 500

Note: phase4 itself never creates SOAP Faults. Per ebMS 3.0 Core, errors of the ebMS layer are expressed as eb:Error signal messages - phase4 only uses the constants above to derive the HTTP status code of the error response.

Received SOAP Faults

Since v4.6.0

Even though ebMS 3.0 Core requires ebMS layer faults to be expressed as eb:Error signals, a peer may still respond with a plain SOAP Fault - e.g. because the message did not even reach the ebMS layer, or because the remote implementation behaves differently. Before v4.6.0 such a response was misreported as "no Signal Message received" and - depending on the HTTP status code - retried pointlessly.

Since v4.6.0 a SOAP Fault received as the synchronous response to an outgoing AS4 message is explicitly detected, classified and surfaced to the caller.

Detection

The detection is implemented in AS4SoapFault.getSoapFaultElementOrNull(Document) and works as follows:

  • The XML root element must be a SOAP Envelope of a known SOAP version - both the SOAP 1.1 and the SOAP 1.2 namespace URI are accepted, no matter which SOAP version the conversation itself uses
  • The first element child of the SOAP Body must be the Fault element of the same namespace
  • The match is done on namespace URI and local name only - never on the namespace prefix
  • The detection happens independent of the HTTP response status code, because non-conforming peers may return a SOAP Fault with HTTP 200
  • A multipart response is never checked, because a SOAP Fault can not be contained in one

To parse a detected fault, use AS4SoapFault.createOrNull(Document) respectively AS4SoapFault.createOrNull(Document, String) - the second parameter is the raw XML to be kept for dumping; if it is null, the provided document is serialized on demand.

The class AS4SoapFault

AS4SoapFault (package com.helger.phase4.model.soapfault) is an immutable, SOAP version aware domain object with the following properties:

Method SOAP 1.1 source SOAP 1.2 source
getSoapVersion() the namespace URI of the Fault element the namespace URI of the Fault element
getFaultCode() faultcode Code/Value
getFaultSubcode() (not available) Code/Subcode/Value
getFaultReason() faultstring the first Reason/Text
getFaultActorRole() faultactor Role
getDetailElement() detail Detail
getRawXML() the full response document the full response document

Fault codes and subcodes are returned as javax.xml.namespace.QName, with the eventually contained namespace prefix resolved against the namespace context of the element it was read from. An unresolvable prefix leads to a QName with the local part only. All properties except the SOAP version and the raw XML may be null if they were absent or empty.

Retry disposition

The method AS4SoapFault.getDisposition() maps the fault code onto the enum EAS4FaultDisposition, to tell whether re-sending the very same message can help at all:

Fault code (local part) Disposition
Client (SOAP 1.1, including dot separated subcategories like Client.Authentication) PERMANENT
Sender (SOAP 1.2) PERMANENT
VersionMismatch PERMANENT
MustUnderstand PERMANENT
Server (SOAP 1.1) / Receiver (SOAP 1.2) TRANSIENT
unknown or absent fault code TRANSIENT

EAS4FaultDisposition offers isPermanent(), isTransient(), getID() (permanent / transient) and getFromIDOrNull(String).

Effect on the HTTP retries

The effect of a received SOAP Fault on the HTTP retries depends on the configuration property phase4.http.response.accept.allstatuscodes (see Configuration):

  • phase4.http.response.accept.allstatuscodes=true (the default): every HTTP response is processed, no matter its status code. A SOAP Fault response therefore terminates the sending regularly - no retry is performed at all, and the fault is surfaced to the caller.
  • phase4.http.response.accept.allstatuscodes=false: a response with a status code ≥ 300 leads to an exception, which normally triggers the configured HTTP retries. In that case:
    • a fault with a PERMANENT disposition raises an AS4SoapFaultException that stops all remaining retries immediately (behavioural change in v4.6.0 - previously all configured retries were exhausted)
    • a fault with a TRANSIENT disposition keeps the regular retry handling; the fault is nevertheless surfaced to the caller

AS4SoapFaultException (package com.helger.phase4.model.soapfault) carries the received AS4SoapFault (via getSoapFault()) and the AS4 message ID of the sent message (via getSentMessageID() / hasSentMessageID()). It deliberately extends java.io.IOException and not Phase4Exception, so that it can pass through the Apache HttpClient response handler and the HTTP retry handling, whose signatures only allow IOException. On the level of the message builders it is translated into a Phase4Exception with isRetryFeasible() == false.

Reacting on a received SOAP Fault

A SOAP Fault is delivered to an IAS4SoapFaultConsumer (package com.helger.phase4.sender):

void handleSoapFault (@Nullable String sSentMessageID,
                      @NonNull AS4SoapFault aSoapFault,
                      @Nullable AS4ClientSentMessage <byte []> aClientSentMessage) throws Phase4Exception;

It is registered on the user message builders via AbstractAS4UserMessageBuilder.soapFaultConsumer(IAS4SoapFaultConsumer):

final EAS4UserMessageSendResult eResult = Phase4PeppolSender.builder ()
                                                            ...
                                                            .soapFaultConsumer ( (sMessageID, aSoapFault, aSentMsg) -> {
                                                              LOGGER.error ("Received a SOAP Fault for message '" +
                                                                            sMessageID +
                                                                            "' with code " +
                                                                            aSoapFault.getFaultCode () +
                                                                            " and disposition " +
                                                                            aSoapFault.getDisposition ());
                                                            })
                                                            .sendMessageAndCheckForReceipt ();

Notes:

  • If no consumer is registered, the received SOAP Fault is logged on error level by the default implementation LoggingAS4SoapFaultConsumer
  • sSentMessageID may be null if the AS4 message ID could not be determined
  • aClientSentMessage may be null if the fault interrupted the sending process before that context was created (this is the case if a PERMANENT fault stopped the retries)

Overall sending result

The enum EAS4UserMessageSendResult, returned by sendMessageAndCheckForReceipt(), has the additional constant SOAP_FAULT_RECEIVED (ID soap-fault-received) since v4.6.0. It is returned if the synchronous response was a plain SOAP Fault instead of an ebMS Signal Message. Whether a retry is feasible depends on the disposition of the fault - register an IAS4SoapFaultConsumer to access it.

Additionally the new enum EAS4ResponseType (package com.helger.phase4.model) classifies the synchronous response of an outgoing AS4 UserMessage. The result is written to the log:

Constant ID Meaning
RECEIPT receipt An ebMS Signal Message containing a Receipt was received
EBMS_ERROR ebms-error An ebMS Signal Message containing at least one Error was received
SOAP_FAULT soap-fault A plain SOAP Fault (1.1 or 1.2) was received
EMPTY empty The response body was empty
UNPARSABLE unparsable The response body was no usable SOAP or contained no usable ebMS message (e.g. an HTML proxy error page)

Serialization

For logging, storing and reporting, AS4SoapFault can be serialized in a standardized way:

  • getAsJsonObject() returns an IJsonObject with the keys soapVersion, faultCode, faultSubcode, faultReason, faultActorRole, faultDetail, rawXML and disposition
  • getAsMicroElement(String sNamespaceURI, String sTagName) returns an IMicroElement with the child elements SoapVersion, FaultCode, FaultSubcode, FaultReason, FaultActorRole, FaultDetail and Disposition

In both cases only the elements that are present are contained and QNames are serialized in Clark notation ({namespaceURI}localPart). The JSON serialization contains the raw XML since v4.6.2; the XML serialization does not contain it - use getRawXML() to access it.

Example JSON:

{
  "soapVersion": "1.2",
  "faultCode": "{http://www.w3.org/2003/05/soap-envelope}Sender",
  "faultReason": "Invalid message",
  "rawXML": "<env:Envelope xmlns:env=\"http://www.w3.org/2003/05/soap-envelope\">...</env:Envelope>",
  "disposition": "permanent"
}

The static method AS4SoapFault.createFromJsonOrNull(IJsonObject) (since v4.6.2) converts such a JSON object back into an AS4SoapFault. The contained disposition is not read back, because it is solely derived from the fault code, and the namespace prefixes of faultCode and faultSubcode are not restored, because they are not part of the Clark notation. null is returned, if the provided JSON object is null or if it contains no valid soapVersion.

Sending reports

The sending report classes of the profiles support a received SOAP Fault as well, via the methods getAS4SoapFault(), hasAS4SoapFault() and setAS4SoapFault(AS4SoapFault):

If a SOAP Fault is set, it becomes part of the report serialization - as the JSON object as4SoapFault respectively as the XML element AS4SoapFault.

The phase4-peppol-server-webapp and the phase4-dbnalliance-server-webapp show the usage: they register a soapFaultConsumer that simply pushes the received fault into the sending report.

Dumping

The raw XML of a received SOAP Fault is routed through the regular incoming dumper (IAS4IncomingDumper) - either the one provided for the sending, or the globally registered one from AS4DumpManager. Dumping requires the AS4 message ID of the sent message to be known; if it is not, a warning is logged instead.

Clone this wiki locally