-
Notifications
You must be signed in to change notification settings - Fork 64
SOAP Handling
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:Messagingheader element and the WS-Securitywsse:Securityheader element reside in the SOAPHeader - the payload resides either in the SOAP
Bodyor - 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)
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)andgetFromVersionOrDefault(String, ESoapVersion)- based on the version string (1.1or1.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).
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).
For incoming messages the SOAP version is determined dynamically:
- For multipart messages: first from the
Content-Typeof 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-Typeof 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 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.
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.
The detection is implemented in AS4SoapFault.getSoapFaultElementOrNull(Document) and works as follows:
- The XML root element must be a SOAP
Envelopeof 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
Bodymust be theFaultelement 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.
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.
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).
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
PERMANENTdisposition raises anAS4SoapFaultExceptionthat stops all remaining retries immediately (behavioural change in v4.6.0 - previously all configured retries were exhausted) - a fault with a
TRANSIENTdisposition keeps the regular retry handling; the fault is nevertheless surfaced to the caller
- a fault with a
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.
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 -
sSentMessageIDmay benullif the AS4 message ID could not be determined -
aClientSentMessagemay benullif the fault interrupted the sending process before that context was created (this is the case if aPERMANENTfault stopped the retries)
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) |
For logging, storing and reporting, AS4SoapFault can be serialized in a standardized way:
-
getAsJsonObject()returns anIJsonObjectwith the keyssoapVersion,faultCode,faultSubcode,faultReason,faultActorRole,faultDetail,rawXMLanddisposition -
getAsMicroElement(String sNamespaceURI, String sTagName)returns anIMicroElementwith the child elementsSoapVersion,FaultCode,FaultSubcode,FaultReason,FaultActorRole,FaultDetailandDisposition
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.
The sending report classes of the profiles support a received SOAP Fault as well, via the methods getAS4SoapFault(), hasAS4SoapFault() and setAS4SoapFault(AS4SoapFault):
-
Phase4PeppolSendingReport(see Profile Peppol) -
Phase4DBNAllianceSendingReport(see Profile DBNAlliance) -
Phase4HREdeliverySendingReport(see Profile HR eDelivery)
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.
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.
My personal Coding Styleguide | It is appreciated if you star the GitHub project if you like it.
