Problem description

The standard SOAPFault that is generated by the CXF framework did not meet my needs. I needed a fault that looks like this:

<soap:Fault>
    <faultcode>FailedAuthentication</faultcode>
    <faultstring>The security token could not be authenticated or authorized</faultstring>
    <detail>
      {complicated detail structure}
      [...]
    </detail>
  <soap:Fault>

By default, no detail is included.

Solution

Since the SOAPFault that I am tracking is actually generated by the framework, I don’t have the ability to modify the exception that is being thrown.

The answer lies in the concept of CXF interceptors. Each request that comes into a CXF web service go through a series of interceptors. There are four interceptor chains: inbound, outbound, inbound fault and outbound fault.

By default, the last interceptor in the outbound fault chain is Soap11FaultOutInterceptor. Looking at the code shows that it automagically appends the details of the SOAPFault into the response. All we have to do is fill in the details of the SOAPFault. There are many places that we could do this, but I decided to do it right before the final interceptor is called.

I added a new interceptor to the outbound fault chain in my Spring configuration file:

<jaxws:endpoint address="/my/endpoint">
  <jaxws:implementor>
    <bean class="ca.intelliware.sample.webservice.QueryFulfiller" />
  </jaxws:implementor>
  <jaxws:outFaultInterceptors>
    <bean class="ca.intelliware.sample.webservice.CustomSoap11FaultOutInterceptor" />
  </jaxws:outFaultInterceptors>
</jaxws:endpoint>

And our code looks like this:

public class CustomSoap11FaultOutInterceptor extends AbstractSoapInterceptor {

    public CustomSoap11FaultOutInterceptor() {
        super(Phase.MARSHAL);
    }

    public void handleMessage(SoapMessage message) throws Fault {
        Fault fault = (Fault) message.getContent(Exception.class);
        fault.setDetail(createDetailSomehow());
    }

    [...]
}

The only tricky part is that we’ve put the new interceptor in the MARSHAL phase. That’s the same phase as the default Soap11FaultOutInterceptor. A SOAP fault will go through our interceptor first then pass the fault into the default interceptor, which will write out the fault as required.

This is just one example of manipulating the behaviour of CXF using interceptors. See Provider Services and WS-Security for an example of adding interceptors to the inbound chain. As long as you’re prepared to get your hands dirty and figure out where your interceptor belongs in the chain, you should be able to modify much of the standard CXF behaviour to meet your needs.

More details

If you’d like to have a little more control about where your interceptor sits in the chain, specify which other interceptors must run before and/or after your interceptor.

This code will ensure that your custom interceptor will run after the default SOAP fault interceptor:

public CustomSoap11FaultOutInterceptor() {
        super(Phase.MARSHAL);
        getAfter().add(Soap11FaultOutInterceptor.class.getName());
    }

It’s a little non-intuitive, but it appears to work.

And this code ensure that your interceptor will run first:

public CustomSoap11FaultOutInterceptor() {
        super(Phase.MARSHAL);
        getBefore().add(Soap11FaultOutInterceptor.class.getName());
    }

Other design considerations

I also wanted the ability to bypass the default SOAP fault interceptor. There are at least two ways to do this.

The first method is to simply manipulate the interceptor chain in the message when you’re dealing with it.

public void handleMessage(SoapMessage message) throws Fault {
    	Interceptor<? extends Message> defaultInterceptor = null;
    	for (Iterator<Interceptor<? extends Message>> iterator = message.getInterceptorChain().getIterator(); iterator.hasNext();) {
    		Interceptor<? extends Message> interceptor = iterator.next();
    		if (interceptor instanceof Soap11FaultOutInterceptor) {
    			defaultInterceptor = interceptor;
		    }
		}
    	message.getInterceptorChain().remove(defaultInterceptor);

The second approach is to modify the cxf-extension-soap.xml configuration to use a different SoapBindingFactory. The default SOAP interceptor gets added in when the SOAP binding is created, and it’s a fairly simple matter to make our own version of the factory that doesn’t add the default.

I ended up using the first method. My reasoning was that different endpoints would behave differently, so it made more sense to remove the default interceptor on a case-by-case basis.