I get a large number of people asking me how to make SOAP web service requests using ColdFusion features like CFInvoke and CreateObject( "webservice" ). Typically, these methods works just as advertised, encapsulating SOAP requests (and responses) in a clean, easy to use ColdFusion wrapper. Of course, that's not the cases that these people are asking me about; typically, people want to know how to get CFInvoke or CreateObject() to work with a particularly complicated API.
There's no doubt that ColdFusion has some excellent (if only slightly outdated) SOAP functionality. But when things get too complicated, I find it's often easier to execute and maintain SOAP requests when you drop down into the raw XML and manual HTTP post. This kind of SOAP handling gives you complete control over all aspects of both the request and the response and leaves no mystery as to how data types should be translated.
To demonstrate this, let me walk through a SOAP request to Campaign Monitor's newsletter subscription API. Given any WSDL file, you should be able to figure out (with some determination) what kind of SOAP request a particular method call is expecting. Depending on the complexity of the given WSDL file, this could be a simple task or a total nightmare. Luckily, most professional APIs provide solid documentation complete with sample XML request and response values. The following screenshot is taken directly off of the Campaign Monitor API url and is typical (in nature) of the kind of documentation I have found with many API providers:
As you can see, the first XML document is a sample of the SOAP request you need to make; the second XML document is a sample of the SOAP response you will get. In addition to the XML, the API documentation also defines the SOAPAction that must be included with the request - take note of this as it will become a required Header value in our CFHTTP post.
Once we have this documentation in hand, all we have to do is formulate our own XML document and post it to the API using ColdFusion's CFHTTP and CFHTTPParam tags:
<!--- We are going to subscribe to Campaing Monitor using the AddAndResubscribe actions. This is a SOAP-based method that requires the following XML body. ---> <cfsavecontent variable="soapBody"> <cfoutput> <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <Subscriber.AddAndResubscribe xmlns="http://api.createsend.com/api/"> <ApiKey>#campaignMonitorKey#</ApiKey> <ListID>#campaignMonitorList#</ListID> <Email>email@example.com</Email> <Name></Name> </Subscriber.AddAndResubscribe> </soap:Body> </soap:Envelope> </cfoutput> </cfsavecontent> <!--- Now that we have our SOAP body defined, we need to post it as a SOAP request to the Campaign Monitor website. Notice that when I POST the SOAP request, I am NOT required to append the "WSDL" flag to the target URL (this is only required when you actually want to get the web service definition). ---> <cfhttp url="http://api.createsend.com/api/api.asmx" method="post" result="httpResponse"> <!--- Most SOAP action require some sort of SOAP Action header to be used. ---> <cfhttpparam type="header" name="SOAPAction" value="http://api.createsend.com/api/Subscriber.AddAndResubscribe" /> <!--- I typically use this header because CHTTP cannot handle GZIP encoding. This "no-compression" directive tells the server not to pass back GZIPed content. ---> <cfhttpparam type="header" name="accept-encoding" value="no-compression" /> <!--- When posting the SOAP body, I use the CFHTTPParam type of XML. This does two things: it posts the XML as a the BODY and sets the mime-type to be XML. NOTE: Be sure to Trim() your XML since XML data cannot be parsed with leading whitespace. ---> <cfhttpparam type="xml" value="#trim( soapBody )#" /> </cfhttp> <!--- When the HTTP response comes back, our SOAP response will be in the FileContent atribute. SOAP always returns valid XML, even if there was an error (assuming the error was NOT in the communication, but rather in the data). ---> <cfif find( "200", httpResponse.statusCode )> <!--- Parse the XML SOAP response. ---> <cfset soapResponse = xmlParse( httpResponse.fileContent ) /> <!--- Query for the response nodes using XPath. Because the SOAP XML document has name spaces, querying the document becomes a little funky. Rather than accessing the node name directly, we have to use its local-name(). ---> <cfset responseNodes = xmlSearch( soapResponse, "//*[ local-name() = 'Subscriber.AddAndResubscribeResult' ]" ) /> <!--- Once we have the response node, we can use our typical ColdFusion struct-style XML node access. ---> <cfoutput> Code: #responseNodes[ 1 ].Code.xmlText# <br /> Success: #responseNodes[ 1 ].Message.xmlText# </cfoutput> </cfif>
Here, we are defining our SOAP request in the content buffer, soapBody. This XML variable is then posted to the Campaign Monitor API using CFHTTP. Notice that when I post to the API, I am not using the "WSDL" URL flag; this is only needed when we actually want to retrieve the web service definition (as is typically required by the ColdFusion SOAP wrappers). Since we are posting the raw XML, no additional web service definition is required.
The SOAPAction value that I mentioned before is now being included as a Header value using ColdFusion's CFHTTPParam tag. If you look at the previous screenshot of the API sample, you will notice that the SOAPAction value is surrounded by double quotes. While this is not required for the Campaign Monitor API, I am pretty sure that I remember running into a few situations where adding the quotes to the SOAPAction was critical:
<cfhttpparam type="header" name="SOAPAction" value="""....""" />
Notice the extra, escaped double quotes in the Value attribute.
Once we have posted the SOAP XML, we need to handle the SOAP response. Assuming that the response was formed well (valid XML) and that the target server was available, your SOAP request should always come back with a 200 response code, regardless of its execution success; any errors within the API request will be defined in the result nodes of the response. While I am not 100% sure that all SOAP APIs act this way, I have never found one to deviate.
When the SOAP response comes back, it should look something like this:
As a final caveat, because the SOAP XML response has name-spaced nodes, querying the document becomes a bit more complicated; rather than using standard node names in your XPATH, you have to query for "*" (any node) and check its local-name(). I don't care for this approach, but it seems to be the easiest way to deal with name spaces.
When we run the above code, we get the following output:
As you can see, we were able to post the SOAP XML and parse its response without any problems.
ColdFusion provides some really great SOAP functionality; API wrappers like CFInvoke and CreateObject( "webservice" ) allow for seamless integration of SOAP web service requests into your ColdFusion code. When APIs get more complicated, however, these wrappers start to break down. Luckily, in times like that, ColdFusion also makes it easy for us to drop down into the raw XML and make manual SOAP HTTP requests. And, if you encapsulate all of this into a ColdFusion component, you end up, once again, with a nice API wrapper.
Want to use code from this post? Check out the license.