Thursday, August 6, 2015

Exception Handling Issues for SOAP Faults with CXF Clients

The Apache CXF product is a wonderful product and for the most part it is easy to use.  It's very common to use CXF to generate clients for SOAP services.  Support for those clients for SOAP services that specify faults (the SOAP version of an exception) is not easy.

For example, when CXF generates exceptions for SOAP faults, which it does, it embeds details of the fault in a field on the exception that isn't reported in either the message or the stack trace.  This information is critical for support developers diagnosing problems and issues.  For example, I generated CXF clients on  a public SOAP service that utilizes faults.  It generated the following exception:

Generated Exception excerpt:
public class BatchException_Exception extends Exception { 
    private com.postini.pstn.soapapi.v2.automatedbatch.BatchException batchException;

public com.postini.pstn.soapapi.v2.automatedbatch.BatchException getFaultInfo() {
        return this.batchException;
    }

}

The meat of the exception - the information you need to actually solve the issue - is in that BatchException class that contains a useful message.  Unfortunately, the generated CXF exception doesn't provide this information in either getMessage() or printSTackTrace().  Here's what the trace looks like; it doesn't contain the embedded CXF diagnostic information:

com.postini.pstn.soapapi.v2.automatedbatch.BatchException_Exception: exception message at org.force66.cxfutils.CxfSoapFaultRuntimeExceptionTest.testBasic(CxfSoapFaultRuntimeExceptionTest.java:28) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)


If you want this information, you have to programatically dig it out.  This isn't hard, but a typical SOAP service will have numerous exceptions. There's no consistent interface implemented, so it's hard to write generic code that will provide the useful fault information when errors occur.  It's a lot of custom code to handle each type of fault.  What a pain.  I've figured out a way to solve this problem.

Fortunately, CXF does allow you to specify the base exception it uses.  java.lang.Exception is the default.  I've created an exception that is meant to be used as a root exception by CXF.  It uses reflection to dig the embedded information out of the CXF exception and make sure it's in the exception message and printStackTrace.  In addition, I leverage the ContextedRuntimeException from Apache Commons Lang as that exception makes it easy to attach useful information to exceptions.  Here's a sample of the same exception output with the new base exception.  Note that the embedded CXF diagnostic information is present.

com.postini.pstn.soapapi.v2.automatedbatch.BatchException_Exception: exception message
Exception Context:
[1:cxfclient.message=embedded cxf info]
---------------------------------
at org.force66.cxfutils.CxfSoapFaultRuntimeExceptionTest.testBasic(CxfSoapFaultRuntimeExceptionTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

The exception that provides this functionality extends ContextedRuntimeException.  It then populates the exception context with the information CXF embeds so that it will appear in your logs without any manual programming effort.  

Information on how to specify the root exception CXF uses can be found here.

This code has been open sourced and is available on github here.  It also has been deployed to Maven and is easily includable in your projects.

/**
 * Exception meant to be extended by Apache CXF when generating exceptions for
 * SOAP Faults. This exception will insure that embedded information CSF places
 * for exceptions will be logged.
 *
 * @author D. Ashmore
 *
 */
abstract public class CxfSoapFaultRuntimeException extends
ContextedRuntimeException {

// constructors omitted for brevity

protected void checkExceptionContextInfo() {
if (!contextAdded) {
Object embeddedInfo = ReflectUtils.safeInvokeExactMethod(this,
"getFaultInfo");
ReflectUtils.reflectionAppendContextValues(this, embeddedInfo,
"cxfclient");
contextAdded = true;
}
}

@Override
public String getMessage() {
checkExceptionContextInfo();
return super.getMessage();
}

@Override
public String getRawMessage() {
checkExceptionContextInfo();
return super.getRawMessage();
}

}