A few months ago I used Spring Web Services to build a POX (Plain Old XML, not SOAP) web service for work. It was a good experience, and Spring-WS made creating a POX service pretty easy. The sample application provided with the Spring-WS dependencies download has a POX application that’s a good seed for starting a project.
Following the example of the sample application worked great for happy path testing. What wasn’t so straightforward was handling errors. You can use an ExceptionResolver to handle problems your code encounters when processing a request. However, it wasn’t obvious from the example how you could return your own error messages when the client sends bad XML. Here’s an approach for elegant handling of those situations.
These examples use the POX Contacts sample from Spring WS 1.5.9.
CountContacts Service
The POX CountContacts sample contains a simple service, that when passed an XML document containing a list of contacts, returns the number of contacts in that list. A contact is just made up of a person’s name and phone number. Here’s a sample request, pulled straight from the Spring WS example.
<?xml version="1.0" encoding="UTF-8"?> <Contacts xmlns="http://www.springframework.org/spring-ws/samples/pox"> <Contact> <Name>John Doe</Name> <Phone>626-555-3456</Phone> </Contact> <Contact> <Name>Jane Doe</Name> <Phone>626-555-6543</Phone> </Contact> </Contacts>
And the response from the service tells you there were two contacts:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <ContactCount xmlns="http://www.springframework.org/spring-ws/samples/pox">2</ContactCount>
Pretty cool. But if clients start sending XML with errors, things begin to fall apart.
A Lenient XSD
Let’s first start with an XSD validation error. Here’s the XSD provided in the Spring-WS POX example.
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.springframework.org/spring-ws/samples/pox"> <xsd:element name="Contacts"> <xsd:annotation> <xsd:documentation> Defines a contact-list, with names and phone numbers. </xsd:documentation> </xsd:annotation> <xsd:complexType> <xsd:sequence> <xsd:element name="Contact" minOccurs="0" maxOccurs="unbounded"> <xsd:complexType> <xsd:all> <xsd:element name="Name" type="xsd:string"/> <xsd:element name="Phone" type="xsd:string"/> </xsd:all> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="ContactCount" type="xsd:integer"/> </xsd:schema>
This XSD is very lenient. It tells us that a ContactList can have zero or more Contact elements, and that each Contact can optionally contain Name and Phone strings. It follows Postel’s robustness principle – “be conservative in what you send, liberal in what you accept” – and that’s probably OK for such a simple service. But for something more complex, you might consider Mark Rose’s rebuttal to the robustness principle, and impose more restrictions.
The ContactStore Service and a Strict XSD
As an example, I’ll add a new Contact storage service that you could use to store names and phone numbers. Since a call to store a contact isn’t much good without a name and a phone number, my XSD is going to be more strict:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.springframework.org/spring-ws/samples/pox"> <xsd:element name="Contacts"> <xsd:annotation> <xsd:documentation> Updates the contact list, by requiring a name an a phone number. </xsd:documentation> </xsd:annotation> <xsd:complexType> <xsd:sequence> <xsd:element name="Contact" minOccurs="1" maxOccurs="unbounded"> <xsd:complexType> <xsd:sequence> <xsd:element name="Name" type="xsd:string" minOccurs="1" maxOccurs="1"/> <xsd:element name="Phone" type="xsd:string" minOccurs="1" maxOccurs="1"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="ContactCount" type="xsd:integer"/> </xsd:schema>
In this XSD, at least one Contact is required, and each Contact is required to have one and only one Name and one and only one Phone.
Here’s the Spring-WS endpoint for a ContactStore. It’s basically a copy of the ContactCount service, which stores the contacts (OK, there’s just a comment about actually storing the contacts – you write it if you want it so bad), and returns the count of the contacts it stored.
package org.springframework.ws.samples.pox.ws; import ... public class ContactStoreEndpoint extends AbstractSaxPayloadEndpoint { private static final String NAMESPACE_URI = "http://www.springframework.org/spring-ws/samples/pox"; private static final String CONTACT_NAME = "Contact"; private static final String CONTACT_COUNT_NAME = "ContactCount"; private DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); public ContactStoreEndpoint() { documentBuilderFactory.setNamespaceAware(true); } protected ContentHandler createContentHandler() throws Exception { return new ContactStoreResults(); } protected Source getResponse(ContentHandler contentHandler) throws Exception { ContactStoreResults results = (ContactStoreResults) contentHandler; DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); Document response = documentBuilder.newDocument(); Element contactStoreElement = response.createElementNS(NAMESPACE_URI, CONTACT_COUNT_NAME); response.appendChild(contactStoreElement); contactStoreElement.setTextContent(Integer.toString(results.contactCount)); return new DOMSource(response); } private static class ContactStoreResults extends DefaultHandler { private int contactCount = 0; public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (NAMESPACE_URI.equals(uri) && CONTACT_NAME.equals(localName)) { contactCount++; //store contact somewhere... } } } }
We’ll also need to update the Spring context with our new endpoint by adding this line to spring-ws-servlet.xml
<bean id="contactStoreEndpoint" class="org.springframework.ws.samples.pox.ws.ContactStoreEndpoint"/>
And we’ll also change the default endpoint to the ContactStore.
<property name="defaultEndpoint" ref="contacStoreEndpoint"/>
Handling XSD Validation Errors
Let’s pretend we have a new customer signed on for our contact service, and their client code has a bug in it that so that the Phone element isn’t added to a contact. So their request just has the Name in it, like this.
<?xml version="1.0" encoding="UTF-8"?> <Contacts xmlns="http://www.springframework.org/spring-ws/samples/pox"> <Contact> <Name>John Doe</Name> </Contact> </Contacts>
How does the sample POX application’s PayloadValidatingInterceptor handle this bad request with the more strict XSD? Well, when I submit this with the Firefox Poster extension, I get this.
Status: 200 OK <?xml version="1.0" encoding="UTF-8" standalone="no"?>
Not good. The service just told the client everything was OK with a HTTP 200 code, but there was really an error in the request. We have to look in the logs to see what the problem was.
May 14, 2011 12:39:19 PM org.springframework.ws.soap.server.endpoint.interceptor.AbstractFaultCreatingValidatingInterceptor handleRequestValidationErrors WARNING: XML validation error on request: cvc-complex-type.2.4.b: The content of element 'Contact' is not complete. One of '{"http://www.springframework.org/spring-ws/samples/pox":Phone}' is expected.
The Phone element is missing, of course. But I don’t want the developers of the new client to contact me and ask me to look at the log file each time they send a request with formatting error. I’d rather have the server tell the client what the error was so I can spend my time writing code, not looking at log files.
Communicating Errors to the Client
But if we want to communicate errors back to our client, we need to make a spot for error messages in the XSD so we can send back errors that validate against the XSD. I’ll add an error element:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.springframework.org/spring-ws/samples/pox"> <xsd:element name="Contacts"> <xsd:annotation> <xsd:documentation> Updates the contact list, by requiring a name an a phone number. </xsd:documentation> </xsd:annotation> <xsd:complexType> <xsd:sequence> <xsd:element name="Contact" minOccurs="1" maxOccurs="unbounded"> <xsd:complexType> <xsd:sequence> <xsd:element name="Name" type="xsd:string" minOccurs="1" maxOccurs="1"/> <xsd:element name="Phone" type="xsd:string" minOccurs="1" maxOccurs="1"/> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="ContactCount" type="xsd:integer"/> <xsd:element name="error" type="xsd:string"/> </xsd:schema>
If you want to customize how Spring-WS handle XSD errors, you need to extend the Spring-WS AbstractValidatingInterceptor class and set that as the validatingInterceptor bean in the Spring context. A ValidatingInterceptor provides hooks for handling validation errors that occur during a POX request or response. Here’s an example that works with the ContactStore example:
package org.springframework.ws.samples.pox.ws; import ... /** * Interceptor that can return an error for XML that * doesn't validate. Note that this only handles messages that fail XSD * validation. Corrupt XML is handled by the PoxMessageDispatcher servlet * * @author lreeder */ public class ContactStoreValidatingInterceptor extends AbstractValidatingInterceptor { private static final String NAMESPACE_URI = "http://www.springframework.org/spring-ws/samples/pox"; @Override protected Source getValidationRequestSource(WebServiceMessage webServiceMessage) { return webServiceMessage.getPayloadSource(); } @Override protected Source getValidationResponseSource(WebServiceMessage webServiceMessage) { return webServiceMessage.getPayloadSource(); } @Override protected boolean handleRequestValidationErrors(MessageContext messageContext, SAXParseException[] errors) throws TransformerException { return handleResponseValidationErrors(messageContext, errors); } @Override protected boolean handleResponseValidationErrors(MessageContext messageContext, SAXParseException[] errors) { //Build up a string showing all the SAX validation errors StringBuilder errorBuilder = new StringBuilder(); for (SAXParseException error : errors) { if(errorBuilder.length() > 0) { errorBuilder.append(" "); } errorBuilder.append(error.getMessage()); } WebServiceMessage errorMessage = createErrorMessage(errorBuilder.toString()); messageContext.setResponse(errorMessage); return false; } public static WebServiceMessage createErrorMessage(String error) { final String ENCODING = "UTF-8"; //TODO - escape XML characters in your error string String errorXml = "<?xml version=\"1.0\" encoding=\"" + ENCODING + "\"?>\n" + "<error xmlns=\"" + NAMESPACE_URI + "\">\n" + error + "</error>\n"; ByteArrayInputStream bs = null; try { //set string encoding to match XML encoding bs = new ByteArrayInputStream(errorXml.getBytes(ENCODING)); } catch (UnsupportedEncodingException e) { //Should never happen because we've set the encoding to UTF-8 // which is supported Just in case, we wrap in a RuntimeException // and rethrow throw new RuntimeException("Error converting converting string to ByteArrayInputStream, encoding " + ENCODING + " is not supported.", e); } WebServiceMessage webServiceMessage = null; DomPoxMessageFactory messageFactory = new DomPoxMessageFactory(); try { webServiceMessage = messageFactory.createWebServiceMessage(bs); } catch (IOException ioe) { //Should never happen because the ByteArrayInputStream is memory-only //Just in case, we wrap in a RuntimeException and rethrow throw new RuntimeException("Error creating encoding error message", ioe); } return webServiceMessage; } }
Note that I’m not replacing potential XML characters (<,&,>) in the error string. You should provide code to do that in a production environment. Now update the Spring context and replace the PayloadValidatingInterceptor in spring-ws-servlet.xml with the ContactStoreValidatingInterceptor:
<bean id="validatingInterceptor" class="org.springframework.ws.samples.pox.ws.ContactStoreValidatingInterceptor"> <description> This interceptor validates both incoming and outgoing message contents according to the 'contacts.xsd' XML Schema file. </description> <property name="schema" value="/WEB-INF/contacts.xsd"/> <property name="validateRequest" value="true"/> <property name="validateResponse" value="true"/> </bean>
Now what happens when we send our bad store request without the Phone element? We get this back:
Status: 200 OK <?xml version="1.0" encoding="UTF-8" standalone="no"?> <error xmlns="http://www.springframework.org/spring-ws/samples/pox"> cvc-complex-type.2.4.b: The content of element 'Contact' is not complete. One of '{"http://www.springframework.org/spring-ws/samples/pox":Phone}' is expected. </error>
Much better! We’ve communicated to the client that there was an error with their request XML, and a phone number was missing. This approach will handle your XSD validation errors, but for the message to even be checked for correctness, it must be valid XML. In the next post, I’ll show you how to communicate errors back to a client who is not even sending good XML.
Pingback: Handling POX Errors in Spring-WS Part 2 | Developer's Garden
Hi,
Can you provide the complete spring-ws-servlet.xml?
I am encountering the following error and not sure what I’ve missed out:
java.lang.IllegalArgumentException: AbstractSoapFaultDefinitionExceptionResolver requires a SoapMessage Object of class [org.springframework.ws.pox.dom.DomPoxMessage] must be an instance of interface org.springframework.ws.soap.SoapMessage
at org.springframework.util.Assert.isInstanceOf(Assert.java:337)
at org.springframework.ws.soap.server.endpoint.AbstractSoapFaultDefinitionExceptionResolver.resolveExceptionInternal(AbstractSoapFaultDefinitionExceptionResolver.java:61)
at org.springframework.ws.server.endpoint.AbstractEndpointExceptionResolver.resolveException(AbstractEndpointExceptionResolver.java:106)
at org.springframework.ws.server.MessageDispatcher.processEndpointException(MessageDispatcher.java:331)
at org.springframework.ws.server.MessageDispatcher.dispatch(MessageDispatcher.java:247)
at org.springframework.ws.server.MessageDispatcher.receive(MessageDispatcher.java:173)
at org.springframework.ws.transport.support.WebServiceMessageReceiverObjectSupport.handleConnection(WebServiceMessageReceiverObjectSupport.java:88)
at org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter.handle(WebServiceMessageReceiverHandlerAdapter.java:59)
at org.springframework.ws.transport.http.MessageDispatcherServlet.doService(MessageDispatcherServlet.java:292)
at com.integrosys.sml.webservice.transport.http.PoxMessageDispatcherServlet.doService(PoxMessageDispatcherServlet.java:32)