Samuel Williams Friday, 06 May 2011

I recently implemented some cross-domain AJAX using jQuery. I wanted to POST data using JavaScript's XMLHttpRequest to another site, and this required the use of the new HTTP access control headers1.

You can try out the functionality on the new site litepanels.co.nz - simply click the "Contact Us" link in the footer and send us message. That form POSTs data to http://www.lucidsystems.co.nz/company/contact-us/send which is responsible for actually sending us a message.

To make this work, you need to respond to the HTTP OPTIONS method with appropriate access control headers. When the client web browser tries to send the XMLHttpRequest, it first initiates an OPTIONS request to the same URI, and checks the headers. These headers specify things such as what domains can make XMLHttpRequests to this URI.

Server-side implementation

Here is an example of how to respond to the HTTP options method using the Utopia framework:


ACCESS_CONTROLS = {
	"Access-Control-Allow-Origin" => "*",
	"Access-Control-Allow-Headers" => "X-Requested-With",
	"Access-Control-Max-Age" => "60"
}

on 'index' do |request, path|
	# ...
	if request.options?
		# Allow AJAX requests from different domains.
		return :status => 200, :headers => ACCESS_CONTROLS
	end
	
	# ...
end

You can check this using curl -i -X OPTIONS $URI:

$ curl -i -X OPTIONS http://www.lucidsystems.co.nz/company/contact-us/index 
HTTP/1.1 200 OK
Date: Fri, 06 May 2011 02:25:28 GMT
Server: Apache/2.2.16 (Debian)
X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.5
<span class="highlight">Access-Control-Allow-Headers: X-Requested-With</span>
<span class="highlight">Access-Control-Allow-Origin: *</span>
<span class="highlight">Access-Control-Max-Age: 60</span>
Content-Length: 0
Status: 200
Content-Type: text/plain

One problem I encountered when executing XMLHttpRequests was the fact that they process 3xx redirections transparently. So, if you return a 3xx redirect, it won't actually return this status code to your handler, but instead process the redirection3. For cross-domain requests, this can be a big problem unless you correctly specify OPTIONS for the redirected page too. Therefore, it is wise to ensure that controllers that process XMLHttpRequests return 2xx or 4xx status codes.

It is also important that controllers return the access control headers.

if request.xhr?
	# You also need to provide access control headers here.
	return :status => :success, :headers => ACCESS_CONTROLS
else
	return redirect(params["from"] ? "success" : "success-no-reply")
end

Client-side implementation

On the litepanels.co.nz, I use JavaScript and jQuery to serialize the contact form and send this data to http://www.lucidsystems.co.nz/company/contact-us/send:

$(function() {
	// The contact form element
	var contact = $("#contact");

	contact.validate({
		rules: {
			subject: "required",
			from: {
				email: true
			},
			message: "required"
		},
		submitHandler: function() {
			$('#contact-popup .main').slideUp();
			$('#contact-popup .sending').slideDown();
			
			$.ajax({
				url: contact.attr('action'),
				type: 'post',
				data: contact.serialize(),
				crossDomain: true,
				headers: {
					"X-Requested-With": "XMLHttpRequest"
				},
				success: function() {
					$('#contact-popup .sending').slideUp();
					$('#contact-popup .success').slideDown();
				},
				error: function(request, status, error) {
					$('#contact-popup .sending').slideUp();
					$('#contact-popup .error').slideDown();
				}
			});
		}
	});
})

jQuery by default doesn't set X-Requested-With for cross-domain requests, so you need to do this manually.

Further Reading

  • Mozilla Developer: HTTP Access Controls
  • WebKit DOM Programming Topics: Security Considerations
  • XMLHttpRequest: Infrastructure for the send method
  • Secure login using AJAX
  • Rack Memory Usage