This is something I've mentioned in the past, but earlier in the week I was reading an article from DZone (which by the way is a darn good tech aggregator and even includes a ColdFusion Zone) about doing cross-domain AJAX calls in PHP. It was a good article, and I thought it would be nice to quickly summarize the issue for ColdFusion developers as well. (And take this as a friendly hint for someone to submit the URL to Dzone. :)
So let's quickly review what in the heck we are talking about. When you make a HTTP request via AJAX, you have to make a request to a URL in the same domain. So an HTML file at foo.com can't use AJAX to hit a resource on goo.com. (One of the cool things about AIR is that you don't have to worry about that.)
The solution (both what I suggest and what PHP Four suggests on their article) is to make use of a server-side proxy. You pass information to this proxy (Hey, I want to hit url X) and the proxy makes the request for you. It then returns the data back to the browser. Let's look at a simple example.
For my JavaScript code I'm using Spry to make a simple request and output part of the response:
<script src="/spryjs/SpryData.js"></script>
<script>
function handleResult(r) {
var resp = r.xhRequest.responseText;
alert(resp.substring(0,250));
}
Spry.Utils.loadURL("GET","http://www.cnn.com",true,handleResult);
</script>
If you run this in your browser you get nothing back in the response as the remote URL is blocked. (Unless you happen to run this at CNN. Where news happens.)
We can bypass this limitation by calling our proxy, like so:
Spry.Utils.loadURL("GET","proxy.cfm?urltarget="+escape('http://www.cnn.com'),true,handleResult);
Notice now I hit a local CFM file, proxy.cfm. I pass the requested URL along as a url variable, urltarget. Let's take a look at this script.
<cfparam name="url.urltarget" default="">
<cfset req = cgi.request_method>
<!--- if req is post, force them to have FORM fields --->
<cfif req is "post" and structIsEmpty(form)>
<cfset req = "get">
</cfif>
<cfif len(url.urltarget)>
<cfhttp url="#url.urltarget#" method="#req#">
<cfif req is "post">
<cfloop item="f" collection="#form#">
<cfhttpparam type="formfield" name="#f#" value="#form[f]#">
</cfloop>
</cfif>
</cfhttp>
<cfoutput>#trim(cfhttp.fileContent)#</cfoutput>
</cfif>
So what's going on here? I first param url.urltarget to ensure it exists. I then check the request method. If the value is not post, it is get (technically more versions are allowed), but for POST requests, I only want to really do a post if I was sent form data.
I then simply perform a CFHTTP. Note that if my request was POST, I loop over the form structure and we send that form data to the remote URL. Lastly I just output the result.
In case your curious, you can do posts via Spry of course:
Spry.Utils.loadURL("POST","proxy.cfm?urltarget="+escape('http://localhost/test3.cfm'),true,handleResult, {postData:"x=2&y=3",headers:{ "Content-Type":"application/x-www-form-urlencoded"}});
This does a POST with form.x and form.y as my values. Lastly, as is noted on the php article, you probably want to look at caching the results to help performance. I'll post a followup later today with an example of this. It's an interesting issue as you have to cache based both on request type and request data. Although you may want to skip caching for form posts altogether.
Archived Comments
As well as caching, you probably do not want to allow connections to any host through your host if you do this 'in anger'.
You could create a cnn-proxy.cfm instead, for instance, that hardcoded the host.
Ajax is good with cf8
I am not sure what lines are in proxy.cfm
proxy.cfm just uses cfhttp to hit the remote site. It then outputs the results.
Hey Ray, I am having a little trouble getting my CF proxy to pars correctly and I am hoping you can help me out. I am using your code for proxy.cfm to proxy a handful of RSS feeds (http://feeds.feedburner.com... for example) and reading them back out with JQuery and JFeed.
...so here's the deal,
when I save the XML locally from the original source, JFeed parses the feed just fine. When I save a copy of feed, as generated by the cfm proxy, it loads just fine. When I make a request directly from proxy.cfm I get a parsing error. Hitting proxy.cfm directly everything looks fine, whatever browser I use sees it as an RSS feed and renders it correctly.
Is it possible that I have some sort of encoding mis-match between what the feed says it is (UTF-8) and whatever my CF server says it is serving up. If so, how do I make sure my encodings all match?
I'm afraid I don't quite get you. You are saying, when you run proxy.cfm in your browser, and hits the remote stuff to get RSS, it works. But your AJAX call to proxy.cfm fails? Have you used Firebug to see what proxy.cfm returns to your client side code?
So I solved my own issue, which ended up being pretty trivial, but likely fairly common. The content type for the proxyed RSS feed needs to text/xml and not text/html.
So in addition to Ray's code, I needed:
<cfheader name="Content-Type" value="text/xml">
...otherwise the HTTP header is wrong, and depending on what you are using to pars the XML and whether or not that parser looks at the header, it will toss a parsing error. JQuery does look at the header.
Hi, can you please tell me how to loop over POST parameters without having a form on the client request. Is there a POST structure with all params?
I'm afraid I don't quite get what you mean. You want to loop over form fields... without having a form?
Sorry Raymond, I explained bad.
What I mean is:
situation 1) POST parameters sent by a form. Method above works perfectly.
situation 2) GET parameters, e.g. sent through a link. I can loop over URL... ok!
situation 3) POST parameters NOT sent by a form (ex. jQuery $.post() call) .... how can I know what parameters are sent ??
Thanks in advance!
When you use jQuery to do a POST, it is like a real form. Your CFM code would still use the FORM scope to process it.
what ever happened to the caching update?
Um - no idea. This was 2 years ago. ;) To be honest, CF9 makes caching pretty trivial. You would just make use of cacheGet/cachePut. Problem solved. :)
ahh...wish it were that easy. not on 9 yet.
Then just store it in the Application scope.