Custom SOAP Faults using Spring WS
There are many situations when you need to write a SOAP-based webservice. Maybe you are writing a test dummy, or maybe you got the interface from some kind of architect. (Yes, there are other reasons, too.) And chances are you’ll be using Spring-WS to do this.
Recently I was doing that, and I found the following inside the interface definition (WSDL):
<element name="faultMessage" type="common:FaultMessage"/>
<message name="faultMessage">
<part name="faultMessage" element="tns:faultMessage"/>
</message>
<portType name="someName">
<operation name="searchOrder">
<input message="tns:searchOrderRequest"/>
<output message="tns:searchOrderResponse"/>
<fault name="faultMessage" message="tns:faultMessage"/>
</operation>
</portType>
That was a rather challenging thing! In case the operation would fail, it should give a SOAP Fault with a custom element in it:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Client</faultcode>
<faultstring xml:lang="en">server exception</faultstring>
<detail>
<ns2:faultMessage xmlns:ns2="http://orders.mycompany.com">
<ns2:code>FOO-042</ns2:code>
<ns2:description>Requested order does not exist</ns2:description>
</ns2:faultMessage>
</detail>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
In cases like these, you need an EndpointExceptionResolver
.
It will translate any exception(s) that your code throws to SOAP Faults.
Spring WS ships with quite a few of them out-of-the-box.
This makes sure that in the end, every Exception
will be taken care of by sending back a generic SOAP Fault.
Rolling your own!
But if you want to customise the SOAP Fault, you need to write your own. Thankfully, this is not hard at all.
By default, Spring WS also ships an abstract base class for EndpointExceptionResolver
instances: AbstractEndpointExceptionResolver
.
It requires you to implement its resolveExceptionInternal
method.
Inside this method, the first thing you’ll typically do is see what kind of Exception
you are dealing with.
For example, if you use exceptions for flow control, you could verify whether it is one of your own Exception
subtypes.
Second, you want to construct the custom details.
As the spec says, it can contain “application specific error information”.
In situations like these, you probably have some JAX-B generated classes for the FaultMessage
(or whatever its name).
Simply create the objects and fill them with details as required, or as you see fit for sending back to clients.
When that is done, we need to add the detail elements to the SOAP fault.
Since the objects that we created in the previous step are instances of JAX-B generated classes, we can use the JAX-B engine to marshall them to XML.
In JAX-B, this is the task of a Marshaller
.
To glue it to the Spring WS context, we need a few steps:
final SoapMessage response = (SoapMessage) messageContext.getResponse();
final SoapBody soapBody = response.getSoapBody();
final SoapFault soapFault = soapBody.addClientOrSenderFault("some human-readable text", Locale.ENGLISH);
final SoapFaultDetail faultDetail = soapFault.addFaultDetail();
final Result result = faultDetail.getResult();
The messageContext
(of type MessageContext
) is something that Spring WS gave us.
The result
(of type Result
) is suitable for use with JAX-B.
Instead of addClientOrSenderFault()
, we could also use addServerOrReceiverFault()
.
It depends on what type of exception we’re dealing with.
Either way, according to the SOAP spec, it should contain a text that is targeted at humans, not at computers.
Finally, it is time to write the faultMessage
to XML using a single call:
marshaller.marshal(FACTORY.createFaultMessage(faultMessage), result);
The return type of the resolveExceptionInternal
method is boolean
.
This value tells Spring WS whether this instance handled the Exception
.
This allows you to write very fine grained resolvers that each resolve specific type(s) of exceptions.
If an EndpointExceptionResolver
instance does not handle the exception, Spring-WS will try a different one.
In the end, it will fall back to the built-in ones if nothing else works out.
Putting it all together
For reference, here is a complete implementation of the EndpointExceptionResolver
interface.
import com.yourcompany.orderservice.generated.FaultMessage;
import com.yourcompany.orderservice.generated.ObjectFactory;
import com.yourcompany.orderservice.OrderNotFoundException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.server.endpoint.AbstractEndpointExceptionResolver;
import org.springframework.ws.soap.SoapBody;
import org.springframework.ws.soap.SoapFault;
import org.springframework.ws.soap.SoapFaultDetail;
import org.springframework.ws.soap.SoapMessage;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.transform.Result;
import java.util.Locale;
/**
* Translates an instance of {@link OrderNotFoundException} to a SOAP Fault.
*/
@Component
public class OrderNotFoundExceptionResolver extends AbstractEndpointExceptionResolver {
private static final Logger log = LoggerFactory.getLogger(OrderNotFoundExceptionResolver.class);
private static final ObjectFactory FACTORY = new ObjectFactory();
private final Marshaller marshaller;
/**
* Prepare the {@link #marshaller} so we can marshall {@link FaultMessage} instances to XML.
* @throws JAXBException In case the JAX-B setup is incorrect.
*/
public OrderNotFoundExceptionResolver() throws JAXBException {
final JAXBContext jaxbContext = JAXBContext.newInstance(FaultMessage.class);
this.marshaller = jaxbContext.createMarshaller();
}
@Override
protected boolean resolveExceptionInternal(final MessageContext messageContext,
final Object endpoint,
final Exception exception) {
if (exception instanceof OrderNotFoundException) {
final OrderNotFoundException onfe = (OrderNotFoundException) exception;
final FaultMessage faultMessage = new FaultMessage();
faultMessage.setCode(cnfe.getCode());
faultMessage.setDescription(cnfe.getDescription());
final SoapMessage response = (SoapMessage) messageContext.getResponse();
final SoapBody soapBody = response.getSoapBody();
final SoapFault soapFault = soapBody.addClientOrSenderFault(
"order not found",
Locale.ENGLISH);
final SoapFaultDetail faultDetail = soapFault.addFaultDetail();
final Result result = faultDetail.getResult();
try {
marshaller.marshal(FACTORY.createFaultMessage(faultMessage), result);
return true; // We have handled the Exception.
} catch (final JAXBException e) {
// Mention what went wrong, but don't fallback or something. Spring will take care of this.
log.error("Could not marshall FaultMessage type", e);
}
}
return false; // We did not handle the Exception. Let's hope somebody else does...
}
}