Posting XML SOAP Requests With jQuery

Posted February 16, 2010 at 10:10 AM

Tags: ColdFusion, Javascript / DHTML

In the jQuery 1.4 Reference Guide by Karl Swedberg and Jonathan Chaffer, it is explained that the processData and contentType properties of a jQuery ajax() request can be adjusted to allow for XML document posts. Typically, AJAX data is serialized into a query string; but, if you set the processData property to false, the data property will be posted as-is. After I read this, I thought it would be fun to see if I could leverage this setting to post XML SOAP requests directly from the client.

 
 
 
 
 
 
 
 
 
 

The immediate problem with posting a SOAP request from the client is that in most cases, the target SOAP web service is located on a 3rd party domain. While this is not an issue for server-side code, this presents a security concern for the client. As such, if you try to post XML to a 3rd party URL directly from the client, you'll get a 403 Forbidden exception. To get around this, we will have to create a SOAP proxy page on our server that accepts SOAP data and passes it along to the target web service.

This SOAP proxy can get complicated, but we're going to keep it very straightforward for our experimentation. The ColdFusion SOAP proxy is going to expect three things:

  • An XML request body. This is the SOAP packet that we'll be re-posting to the target web service.
  • The SOAPTarget header. This is the URL of our 3rd party web service.
  • The SOAPAction header. This is the SOAP action which is required for most every SOAP web service I've ever used.

Once our ColdFusion proxy has this information, it will use the CFHTTP tag to invoke the proxied web service:

soap_proxy.cfm

  • <!--- Get the request data. --->
  • <cfset requestData = getHTTPRequestData() />
  •  
  •  
  • <!---
  • Check to make sure this is a valid SOAP request - we
  • need to make sure that the content is XML and that we have
  • the SOAPTarget and SOAPAction.
  • --->
  • <cfif (
  • isXml( requestData.content ) &&
  • structKeyExists( requestData.headers, "SOAPTarget" ) &&
  • structKeyExists( requestData.headers, "SOAPAction" )
  • )>
  •  
  • <!--- Pass the SOAP request onto the target. --->
  • <cfhttp
  • result="soapResponse"
  • method="post"
  • url="#requestData.headers.SOAPTarget#">
  •  
  • <!--- Set SOAP action header. --->
  • <cfhttpparam
  • type="header"
  • name="SOAPAction"
  • value="#requestData.headers.SOAPAction#"
  • />
  •  
  • <!--- Submit the XML post body. --->
  • <cfhttpparam
  • type="xml"
  • value="#requestData.content#"
  • />
  •  
  • </cfhttp>
  •  
  • <!--- Conver the SOAP response to binary. --->
  • <cfset binaryResponse = toBinary(
  • toBase64( soapResponse.fileContent )
  • ) />
  •  
  • <!--- Stream back to client. --->
  • <cfcontent
  • type="text/xml"
  • variable="#binaryResponse#"
  • />
  •  
  • <cfelse>
  •  
  • <!--- Create a malformed request error response. --->
  • <cfheader
  • statuscode="400"
  • statustext="Bad Request"
  • />
  •  
  • <!--- Create an error message. --->
  • <cfsavecontent variable="responseText">
  • Your SOAP request must:
  • - Be XML Content
  • - Have the SOAPTarget header
  • - Have the SOAPAction header
  • </cfsavecontent>
  •  
  • <!--- Convert the response to binary. --->
  • <cfset binaryResponse = toBinary(
  • toBase64( responseText )
  • ) />
  •  
  • <!--- Stream back to client. --->
  • <cfcontent
  • type="text/plain"
  • variable="#binaryResponse#"
  • />
  •  
  • </cfif>

As you can see, this page grabs the SOAP request information using the getHTTPRequestData() function. This gives our ColdFusion page access to the posted XML content body as well as the HTTP headers. It then takes this information and re-posts it using CFHTTP and CFHTTPParam. Whatever SOAP response comes back from the target web service is simply streamed back to the client using the CFContent tag. Like I said - we're trying to keep this as simple as possible.

With our ColdFusion SOAP proxy in place, we can now look at the client-side jQuery code. The meat of the code is in the ajax() method call:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>Submitting SOAP Requests With jQuery</title>
  • <script type="text/javascript" src="jquery-1.4.1.js"></script>
  • <script type="text/javascript">
  •  
  • // When the DOM is ready, initialize the script.
  • jQuery(function( $ ){
  •  
  • // Get a handle on our SOAP template.
  • var soapTemplate = $( "#soap-template" );
  •  
  • // Get a handle on our form.
  • var form = $( "form:first" );
  •  
  • // Get a handle on our zip code input field.
  • var zip = $( "#zip" );
  •  
  • // Get a hand on our city and state place holders.
  • var city = $( "#city" );
  • var state = $( "#state" );
  •  
  •  
  • // Bind the form submission to re-route through our
  • // SOAP-based AJAX request.
  • form.submit(
  • function( event ){
  • // Prevent the default submit.
  • event.preventDefault();
  • event.stopPropagation();
  •  
  • // Create our SOAP body content based off of
  • // the template.
  • var soapBody = soapTemplate.html().replace(
  • new RegExp( "\\$\\{[^}]+\\}", "i" ),
  • zip.val()
  • );
  •  
  • // Trim the SOAP body so that we don't get any
  • // XML prolog errors.
  • soapBody = $.trim( soapBody );
  •  
  • // Post SOAP request.
  • $.ajax({
  • type: "post",
  • url: "./soap_proxy.cfm",
  • contentType: "text/xml",
  • data: soapBody,
  • dataType: "xml",
  • processData: false,
  • beforeSend: function( xhr ){
  • // Pass the target URL onto the proxy.
  • xhr.setRequestHeader(
  • "SOAPTarget",
  • "http://www.webservicex.net/uszip.asmx"
  • );
  •  
  • // Pass the action onto the proxy.
  • xhr.setRequestHeader(
  • "SOAPAction",
  • "http://www.webserviceX.NET/GetInfoByZIP"
  • );
  • },
  • success: function( response ){
  • // Get a jQuery-ized version of the response.
  • var xml = $( response );
  •  
  • // Populate the city. NOTE: These
  • // node names are case-sensitive and
  • // have to be uppercase.
  • city.text(
  • xml.find( "CITY" ).text()
  • );
  •  
  • // Populate the state. NOTE: These
  • // node names are case-sensitive and
  • // have to be uppercase.
  • state.text(
  • xml.find( "STATE" ).text()
  • );
  • },
  • error: function(){
  • console.log( "ERROR", arguments );
  • }
  • });
  •  
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Submitting SOAP Requests With jQuery
  • </h1>
  •  
  • <form>
  •  
  • <p>
  • Zip Code:
  • <input id="zip" type="text" size="20" />
  • <input type="submit" value="Get Info" />
  • </p>
  •  
  • <p>
  • City: <span id="city"> ... </span><br />
  • State: <span id="state"> ... </span>
  • </p>
  •  
  • </form>
  •  
  • <!--
  • This is the SOAP template that we will use when making
  • our SOAP POST to the server (ColdFusion SOAP Proxy).
  • -->
  • <script id="soap-template" type="application/soap-template">
  •  
  • <?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>
  •  
  • <GetInfoByZIP xmlns="http://www.webserviceX.NET">
  • <USZip>${zip}</USZip>
  • </GetInfoByZIP>
  •  
  • </soap:Body>
  •  
  • </soap:Envelope>
  •  
  • </script>
  •  
  • </body>
  • </html>

Like I said above, we are setting the processData AJAX property to false so that our data property is not serialized. We need to do this because our data property does not contain the standard name-value data points; rather, it contains our raw XML SOAP packet. In order to get our AJAX post ready for our ColdFusion SOAP proxy page, we need to add two headers - SOAPTarget and SOAPAction. We can do this in the beforeSend() AJAX event handler; this event handler gives us access to the given XMLHTTPRequest object, which has methods for setting custom header values.

To demonstrate the XML data being used, I'm going to submit the form with the zip code, "10016." When merged into the script-based SOAP template, this will give us the following XML post:

  • <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>
  •  
  • <getinfobyzip xmlns="http://www.webserviceX.NET">
  • <uszip>10016</uszip>
  • </getinfobyzip>
  •  
  • </soap:body>
  •  
  • </soap:envelope>

After this SOAP request has gone through our ColdFusion proxy, it comes back with the following SOAP response:

  • <?xml version="1.0" encoding="utf-8"?>
  • <soap:Envelope
  • xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  • xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  •  
  • <soap:Body>
  •  
  • <GetInfoByZIPResponse
  • xmlns="http://www.webserviceX.NET">
  •  
  • <GetInfoByZIPResult>
  • <NewDataSet xmlns="">
  • <Table>
  • <CITY>New York</CITY>
  • <STATE>NY</STATE>
  • <ZIP>10016</ZIP>
  • <AREA_CODE>212</AREA_CODE>
  • <TIME_ZONE>E</TIME_ZONE>
  • </Table>
  • </NewDataSet>
  • </GetInfoByZIPResult>
  •  
  • </GetInfoByZIPResponse>
  •  
  • </soap:Body>
  •  
  • </soap:Envelope>

In the onSuccess() AJAX event handler, I am then converting this SOAP response into a jQuery collection. From within this collection, I can then use the find() method to locate the CITY and STATE XML nodes and move their text values into my jQuery / HTML place holders. The XML document is case-sensitive which is why I have to use uppercase CITY and STATE in my jQuery traversal methods.

Due to cross-domain security concerns as well as the highly structured nature of the post data, SOAP requests are not something that lend very well to AJAX requests. However, if you are ever in a situation where this has to be done, it's nice to know that the jQuery ajax() method makes this mostly possible. On a side note, this just goes to show you how good "Reading The Manual" can be; had I not read the jQuery 1.4 Reference Guide, chances are I would have never even thought about posting XML requests with AJAX.




Reader Comments

Feb 28, 2010 at 10:19 PM // reply »
1 Comments

do you have a sample of this using PHP?
thanks.


Mar 1, 2010 at 8:00 AM // reply »
8,836 Comments

@Jing,

I don't know much of anything about PHP.


Mar 18, 2010 at 10:28 PM // reply »
1 Comments

can you please point me to the jquery documentation on the following
# // Create our SOAP body content based off of
# // the template.
# var soapBody = soapTemplate.html().replace(
# new RegExp( "\\$\\{[^}]+\\}", "i" ),
# zip.val());

inparticular the replace method and RegExp object.

I have not been able to find either at http://api.jquery.com/

Thank You


Mar 19, 2010 at 8:26 AM // reply »
8,836 Comments

@Jason,

The RegExp object and the replace method are parts of the core Javascript language; they are not part of jQuery.


Jun 20, 2010 at 8:58 PM // reply »
1 Comments

I there,

Thanks for your post, it's great!
I'm doing something similar but the request needs to be done through ssl and the client needs to send a certificate too. Do you have any idea of how the JavaScript code can be changed to reflect this?

cheers


Jun 20, 2010 at 9:15 PM // reply »
8,836 Comments

@Miguel,

Have you tried just switching to an HTTPS address? As long as the page that contains the jQuery is also HTTPS, I can't see why an SSL connection would fail (in theory - not sure if this is true).


Jul 8, 2010 at 11:09 AM // reply »
1 Comments

I had read a previous blog post of yours about the troubles you were having with getHTTPRequestData and "Premature end of file" errors. Did you resolve or work around that? If yes, how? Tx!


Jul 9, 2010 at 10:27 AM // reply »
8,836 Comments

@Matt,

I *did* find a work-around to the problem... sort of. It's wicked complex and very robust, but it seems to work. I used to intercept "ColdFusion-as-a-Service" requests to grab incoming SOAP images manipulation requests and add my own watermark to the images before they were returned.

http://www.bennadel.com/blog/1774-Intercepting-ColdFusion-As-A-Service-SOAP-And-RESTful-Component-Requests.htm

It came down to actually grabbing the SOAP request, examining it, and then RE-posting it to the local server for the actual processing (then intercepting the return, mutating it, and returning it manually).

Like I said - way overly complicated; but, it's the only thing I could ever come up with.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Formatting: <strong>bold</strong> <em>italic<em>







  • Help Wanted - Find Your Next ColdFusion Job
Recent Blog Comments
Sep 9, 2010 at 12:50 AM
New ColdFusion Error: Form Entries Incomplete Or Invalid
Once again you save my day Ben. Just migrated from Railo to CF9 and that error was everywhere, I was on the verge of tears... ... read »
Sep 8, 2010 at 7:28 PM
What ColdFusion Teaches Us About The Ultimate "Roll Your Own" Solution
This is how good enterprise software and good enterprise architecture is built. In addition to the time factor and the abstraction potential, the most compelling reason I've ever found to incorporate ... read »
Sep 8, 2010 at 4:47 PM
Ask Ben: Reading In A File Using CFFile And CFInclude
@Ben, Thanks for the quick reply. That was the idea. The getFileFromPath returns a "52648.tmp" file name which is generated by the server not the actual file name. I'm not sure how to extract t ... read »
Sep 8, 2010 at 4:10 PM
Strange ColdFusion URLDecode() and GetEncoding() Behavior
Yep - too strange since the second arg in urldecode is optional. I did run across the error and googled it and landed safely here. Thanks, Ben! ... read »
Sep 8, 2010 at 3:33 PM
What ColdFusion Teaches Us About The Ultimate "Roll Your Own" Solution
@Steve, @Darren, Excellent point! Keeping a platform API (any API at that matter) allows for a much easier time to swap underlying libraries, or even to build your own. @Rick, @Jacob, @JC, I thin ... read »
JC
Sep 8, 2010 at 2:02 PM
What ColdFusion Teaches Us About The Ultimate "Roll Your Own" Solution
@Jacob -- absolutely. The trick is knowing when it'll take less time to do it yourself than wedge someone else's oval shaped application into your round hole. ... read »
Sep 8, 2010 at 1:49 PM
What ColdFusion Teaches Us About The Ultimate "Roll Your Own" Solution
@Rick, I hope this isn't too off topic but, is this true? I've only been programming in the workplace for 4 years. I often look at what others have done and adapt it to my particular needs. Often t ... read »
Sep 8, 2010 at 12:39 PM
ColdFusion CFMailParam's New "Content" Attribute Is Awesome
Ben, Mine is version 8 and tried to download the update but still did not recognized content attribute in cfmailparam. May be I installed a wrong update, so many of them not sure which one I need wit ... read »