Tuesday, June 20, 2006

How to set Cookies in ColdFusion SOAP requests

I spent some time today working with a customer who is using a web service that requires its users to set HTTP cookies in their SOAP requests. First off, I think this is just wrong as this is what SOAP headers are used for and ColdFusion MX 7 (6.1 too) can easily set SOAP headers in both requests and responses.

In any case, Apache Axis can include HTTP cookies in the requests it sends out and since ColdFusion uses Axis as its implementation engine, you can do it in CFML also.

First, how would you set a cookie (or cookies) in Java? Here is some sample Java code:

    mystub._setProperty(
HTTPConstants.HEADER_COOKIE,
new String[]{"myCookie1=hello", "myCookie2=goodbye"});

This calls the "_setProperty" function on the Stub object, passing it a string ("Cookie") and an array of strings, which are the cookies to set. It turns out that the Axis code will look at the second argument and if it is a single String, it will set that as a cookie. If it is a String array, then it will treat each string in the array as a different cookie header.

So, how do we duplicate this in CFML?

First, realize that when you create a "webservice" object in CFML, this can be treated as the org.apache.axis.client.Stub class using the CF/Java interop. So in the case of setting a single cookie, we have no problems:


ws = CreateObject("webservice", "path-to-wsdl");
ws._setProperty("Cookie", "myCookie1=hello");

But this doesn't work! Why not? Well for those of you following along in the Apache Axis source tree, you will notice that the org.apache.axis.transport.http.HTTPSender.java class has this code in it (around line 260):

// don't forget the cookies!
// mmm... cookies
if (msgContext.getMaintainSession()) {
fillHeaders(msgContext, HTTPConstants.HEADER_COOKIE, otherHeaders);
fillHeaders(msgContext, HTTPConstants.HEADER_COOKIE2, otherHeaders);
}

So we need one more thing, we have to turn on the "maintain session" switch in the message context. This turns out to be simple to do as there is an API on the Stub to do just that:

ws.setMaintainSession(true); // required so axis will do cookies

Great, we add that and we can set one cookie for any web service operation we invoke. But what if we want to set more than one (this to-be-nameless service requires three!)?

Well, we need to create an array of Java Strings to pass in to _setProperty(). What happens if we try and use a CFML array? Well, usually CF is really good about taking its (mostly) typeless things and converting them to particular java types. But in this case the API signature for _setProperty is this:

public void _setProperty(String name, Object value);

So CF happily passes the CFML Array in to this API as an object. But deep down in the Axis code (HTTPSender.java, line 534 or so), it tries to treat this object either as a string array (String[]) or a String. Neither of which works in this case. You next thought might be to use the JavaCast() CFML function, as this is one of the cases that it is designed for. But unfortunately it only handles scalar (simple) types (yes, we need to enhance that). What next?

Well, in CFML you can create an Java object that you want using CreateObject. It just so happens that the java.lang.reflect.Array class has functions that will create arrays. So we can use the newInstance() function to allocate an array. This function takes in a Class object and the number of elements in the array.

string = CreateObject("java", "java.lang.String");
array = CreateObject("java", "java.lang.reflect.Array");
cookies = array.newInstance(string.getClass(), 3);

This creates a 3 element String array. One bug that this uncovers is that you can not treat this array the same as a CFML array. For instance you can not assign cookies[1] = "x=1" because ColdFusion will report a problem with casting "coldfusion.runtime.Cast$1", which is a really bad error message that makes sense if you could read the source code like I did, but doesn't help at all. Suffice to say that we are doing the wrong thing with a Java array when trying to set a value in that array. So what can we do? The Java Array class comes to the rescue again as we see that it has a set() API that takes an array, the index and the value you want to set. So we can call it like this:

array.set(cookies, 0, "x=1");
array.set(cookies, 1, "x=2");
array.set(cookies, 2, "x=3");

Watch out for the zero (0) indexed Java array, which differs from the one (1) indexed arrays in CFML.

We are then all set, we can use our cookies array as an argument to _setProperty and since we know it is a String[] (and CF wont change that) it will get down to the Axis HTTP class and be sent out in the HTTP request.

Here is the complete cfscript code:

// Create the array for cookies
string = CreateObject("java", "java.lang.String");
array = CreateObject("java", "java.lang.reflect.Array");
cookies = array.newInstance(string.getClass(), 3);
// set the cookie values
array.set(cookies, 0, "cookie1=one");
array.set(cookies, 1, "cookie2=two");
array.set(cookies, 2, "cookie3=three");

ws = CreateObject("webservice", "http://localhost:8500/test.cfc?WSDL");
ws.setMaintainSession(true); // required so axis will do cookies
ws._setProperty("Cookie", cookies);

// invoke an operation
ret = ws.echo("hi");

12 comments:

PaulH said...

hey, i've been asking to beef up javacast for years ;-)

but couldn't you do a toArray() on a cf array to get java to swallow it?

Jeremy said...

Hi, Tom. I've been finding your blog very useful!

I'm also attempting to connect to a web service that requires HTTP cookies. The server on the other end is not picking up all of our cookies we send because it has difficulty with the fact that Axis 1.2.1 apparently writes multiple cookies on multiple lines instead of a semicolon-separated single line.

The company hosting the web service we are trying to connect to released their own patched version of the axis.jar file. So far as I can tell, the only change they made was to the fillHeaders method in the org.apache.axis.transport.http.HTTPSender class. They are forcing the cookies to all be written to one line. See below for the original fillHeaders method vs their modified version.

My question is if there is a way you can replicate this behavior of writing the cookies to a single line without monkeying around changing the axis.jar file. We're reluctant to use a non-standard distribution of axis, and so far as I can tell 1.3 and 1.4 behave similarly.

Thanks.

----

//Axis 1.2.1 fillHeaders:

private void fillHeaders(MessageContext msgContext, String header, StringBuffer otherHeaders)
{
Object ck1 = msgContext.getProperty(header);
if(ck1 != null)
if(ck1 instanceof String[])
{
String cookies[] = (String[])ck1;
for(int i = 0; i < cookies.length; i++)
addCookie(otherHeaders, header, cookies[i]);

} else
{
addCookie(otherHeaders, header, (String)ck1);
}
}

---

//Modified fillHeaders:
private void fillHeaders(MessageContext msgContext, String header, StringBuffer otherHeaders)
{
Object ck1 = msgContext.getProperty(header);
if(ck1 != null)
if(ck1 instanceof String[])
{
String cookies[] = (String[])ck1;
otherHeaders.append(header).append(": ");
for(int i = 0; i < cookies.length; i++)
{
otherHeaders.append(cookies[i]);
if(i < cookies.length - 1)
otherHeaders.append("; ");
}

otherHeaders.append("\r\n");
} else
{
addCookie(otherHeaders, header, (String)ck1);
}
}

Tom said...

Jeremy,

Sounds like a bug in the web server at the other end, but you can always file a bug against Axis 1.4 and request the change to the Axis source.

Employee Learning Solutions said...

Tom,

I just discovered your blog and you have some very helpful postings. I have been reading the web service ones in particular. I have been coding in CF for a while now, but never really had a reason to get into consuming web services. Now I have a project where I am trying this. The vendor of the service I am trying to consume informs me that we have to use cookies in our requests to authenticate the user each time. They sent me some code to do this in C# with CookieContainer, but I am not a C# person. So that is how I stumbled upon this post. I have been able to dump the service methods, etc. to the page. The problem I am having is that I am calling a method and attemping to output that method. Here is what it is outputting...

com.perseus.www.Pdc_WS.GetSurveyListResponseGetSurveyListResult@f478b5

According to the vendor's docs, the method I am using is supposed to return a list in an XML representation. Just note that when I tested it using XML functions, such as IsXML, the return was NO.

Got any ideas or anything I can read to educate myself on how to accomplish this?

Thanks in advance.

Tom said...

@Employee Learning Solutions (nice name, your parents didn't like you? :-) the result object you are getting back is a Java object. This is the object that the Axis engine that ColdFusion uses generated for the operation from the WSDL.

You would treat this object just like a CF Struct - that is if you want to get the firstname out of it, you would use result.firstName (or whatever it was called in the Schema description in the WSDL).

You (especially if you are using CF8) should also use the cfdump tag on this object and you will get a nice picture of what things you can get from it. Just look for any getXXXX functions. For instance, getFirstname() will automatically be called when you reference result.firstname.

Hope that get you started.

Muddu said...

Hi TOM,
Thanks alot for writing this informative document.
I was struggling to maintaina a session on my invoking cfml, all the time not knowing that I have to setMaintainSession() before calling other functions.
I banged my head for almost 25 days, trying everything using CFID, CFTOKEN and UUID as means for checking whether a session exist all the time Unknowingly that the AXIS is not maintaing a session.
Thanks for helping me out on this.

Daniel Abrantes said...

First of all sorry for my intrusion, I'm here because I'm struggling about something that should be easy..

How to get a http headers from a webservice call?

Any though?
Thanks

Tom said...

@Daniel if you are on the server side (publishing a web service via a CFC) you should be able to use the GetHttpRequestData() function (see http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_e-g_43.html).

If you are trying to get the returned headers from a cfinvoke, nothing comes to mind at the moment. Check out the APIs on the web service object in CFML via the cfdump tag and see what you find.

Daniel Abrantes said...

thanks Tom for your anwswer

kenton said...

Thanks for this, I had never seen setMaintainSession before!

My Scribble Pad said...

Hi Tom, I need to send a persistent cookie through the http header. i use org.apache.soap.rpc.Call for invoking the webservice call. I got this code to include the cookie to the URL

URL url = new URL(proxy);
URLConnection conn = url.openConnection();
conn.setRequestProperty("Cookie", persistentCookie);

but i am not sure if i have to do a conn.connect() or not as my next line of code is
Response resp = call.invoke(url, "");

How should i actually include the persistent cookie to the http header? I dont have a http header request or object in my class.

Tom said...

@Scribble Pad - I don't think your question is on topic.

The post explains how to set a cookie using an Axis 1.x web service stub class. You are asking about URLConnection, which is a Java API.