Using Rails AJAX helpers to create safe state-changing links
Posted by #
A few months ago there was a heated discussion going on about Google Web Accelerator prefetching links and at the same time wreaking havoc in web apps that used plain GET links to change the state of an application. A few tricks came up on how one could block GWA from accessing given pages, but in the end, using GET requests for operations such as deleting records in your app remained dangerous.
The traditional means to avoid the perils of GWA and friends are two-fold: either use only form buttons (and thus POST requests) to commit these mission-critical actions, or link to a confirmation page that does the same. Unfortunately, these solutions are less than optimal. Using dozens of forms in a web page (think “delete” links in a product listing) makes the code a bit messy and a plethora of delete buttons doesn’t make the page look very nice, either. The problem with a confirmation page is that it adds one more step to the process and thus makes the user think one more time. One part of the beauty of OS X compared to Windows is that it doesn’t try to intervene in every action I make. I like to adhere to the same standards so I want to leave confirmation pages for situations where I really, really think they are crucial.
If you’re using Ruby on Rails to build your next killer web app, consider yourself lucky. In the following paragraphs, I’m going to teach you how to use the übercool AJAX helpers in Rails to create action links that are both slick, accessible and about as safe as you can get in the wild wide web.
OK, let’s assume you have an app ready that uses the following link_to helper call to destroy an item from your collection of sock monkeys:
1 2 |
<%= link_to "Delete", :controller => "monkey", :action => "delete", :id => monkey.id %> |
This would serve you well, that is, until your uncle Enoch finds an abandoned Google Web Accelerator from the trash bin and your beloved monkeys start evaporating in the thin air. So what’s to the rescue? link_to_remote!
1 2 3 4 5 |
<%= link_to_remote "Delete", :url => {:controller => "monkey", :action => "delete", :id => monkey.id}, :update => "monkeys" %> |
Now we’re talking! You’re from this day on using AJAX in your app. Your monkeys are now destroyed without a refresh of a page, and no GET request is ever made. And as your delete action renders the monkey list when called by AJAX, the list updates itself as if magically. As simple as that. But wait! Aunt Marge (your unpaid tester) yells something behind her AS/400. She can’t use your app, nothing happens in her lynx even though she tries to follow the link. Crap. No javascript.
Fortunately link_to_remote has a fallback system:
1 2 3 4 5 6 7 8 |
<%= link_to_remote "Delete", {:url => {:controller => "monkey", :action => "delete", :id => monkey.id}, :update => "monkeys"}, {:href => url_for(:controller => "monkey", :action => "delete", :id => monkey.id)} %> |
Using the href parameter makes link_to_remote to include a (tada!) href attribute in the anchor tag it creates. You can use url_for to create the address for it just like in normal links.
Ok, time for a little retrospective. What does our little link widget really do? In normal case, when the user has a modern browser with javascript enabled, when clicked, it calls the delete action with an XMLHttpRequest, and upon success updates the element with id “monkeys” in the current page. If javascript is disabled, the browser will follow the traditional href link to the delete page.
Note that both the AJAX url and the old-fashioned href point to the same action. This is intentional. It gives us the possibility to do pretty powerful things with a tiny little action. We’ll take a look at that action, MonkeyController::delete, next.
1 2 3 4 5 6 7 8 9 10 11 |
def delete if request.xhr? # ... deletion code here ... render :partial => "monkey", :collection => @monkeys elsif request.post? # ... deletion code here ... redirect_to :action => "list" else # we assume this is a get message render :action => "delete_confirmation" end end |
The delete function above handles three kinds of requests. If it’s called by AJAX (remember the normal case above?), it deletes the monkey and renders the list of remaining monkeys that will then be displayed in the calling page. If the action is called by a POST request (we’ll get into when this happens in just a few seconds), the monkey is also deleted but this time, the browser is redirected to the original monkey list page with an external redirect. In third case, when this action is called by a GET request, we show the user a confirmation page by rendering them delete_confirmation.rhtml (only the important parts of the template are shown here):
1 2 3 4 5 |
<%= form_tag :controller => "monkey", :action => "delete" %> <%= hidden_field_tag "id", @params[:id] %> <%= submit_tag "Really, please get rid of the monkey" %> <%= end_form_tag %> |
This is the page shown to old-world users with javascript disabled that clicked on the delete link on the monkey list page, resulting in calling the delete action with a GET request. This page is effectively, as you can see, a form that points to the same old delete method. We need to pass the monkey id in the form and we use the convenient hidden_field_tag helper method for that. Requests from this form page constitute the second case of calling delete, making request.post? return true (because POST is the default method for form_tag).
Why is this method cool?
You can use what kind of text or image links on your pages you ever want to, just like with normal links. That’s got to be nicer than a battery of submit buttons that look all different in different browsers. Even cooler, you don’t have to stuff your users through the tunnel of indifferent confirmation pages. Just one click, and away they go!
But that’s not all, folks! The approach is also accesible. People with text browsers or screen readers (or the paranoid with javascript disabled) will be presented a traditional link followed by (sigh!) a confirmation page, for their own safety.
Why is this method safe?
No critical action is made with a GET request. In the most common case the deletion is made through an XMLHttpRequest, which is a) using POST method and b) launched by javascript so robots or Accelerators or other villains couldn’t even invoke it. The fallback method, on the other hand, uses the traditional confirmation page idea, forcing the user to submit a POST form before the actual deletion is made.
So, here’ya go! A complete system for deleting monkeys. Well, maybe not complete but you get the idea. And all with just a single controller method. And like with all AJAX helpers in Rails, it’s really easy to turn a traditional, link- or form-based approach to full-blown AJAX goodness in a matter of minutes.
Disclaimer: The point of this article is not to teach all the goodies of link_to_remote or other AJAX helpers in Rails. There’s a lot better resources for that and I intentionally left the code barebones simple. You’d certainly want to give the user some kind of indication that an AJAX process is underway, for example. Search the Rails wiki and API docs for more info.

absolutely great article! very useful. i certainly will use that with my current e-learning web app project. thank you a lot
This is a very cool pattern. I was just thinking yesterday about how to do something like this. Thanks for putting it together.
A suggestion… repeating the URL information in the link_to_remote call violates the DRY principal. To be totally bitchin you should have a link_to_remote_with_href helper that creates the link_to_remote call for you. (Yes, that name is wordy and should be shorter, but you get the idea.)
very cool. Added this blog entry to my collection of ajax resources, at http://www.rawsugar.com/collections/guyt/ajax
Sounds like a rails worthy convention to me. Great idea – well relayed!
Thanks for your help
This only really solves the problem of dumb spiders or crawlers following links. But what is to prevent someone from actually figuring out the URL for your AJAX web service and then sending requests in that way?
These problems are really only solved with good session handling and proper deletes, inserts and updates (verifying indentity/record ownership before taking action…) but even those ultimately would have limited affect if someone were determined…
Ryan the Google Web Accelerator does it’s damage in the context of the user’s session. Hence the whole point of this blog entry.
Joshua:
I noticed the DRY issue a few weeks ago and submitted a patch for link_to_remote to try to “guess” the href from :url. Ticket here if you’re interested (or bored)
Although this is a good way to show off ajax on rails, wouldn’t it be more practical to show people how to implement access control and authentication?
Jason: Great, thanks for the link!
neomike: This article is not to show off AJAX. You just can’t solve the problem we’re talking about here with authentication. I repeat: no matter how strong authentication you have in your web app, you’re still vulnerable to GWA-like “attacks”. Please read some of the takes on the issue linked in the beginning of this article.
Like I said in a previous comment, this article assumes that you already have a necessary level of authentication implemented in your app. If you want to read more on how to do that, there’s a lot of articles to help you in the Rails wiki.
I really like that you’re addressing graceful degradation, which is something I’ve missed in most Ajax approaches. However, there are browsers out there with Javascript support but without XMLHttpRequest support. Isn’t there a way to support those browsers as well?
Jonas,
All it takes is that the JavaScript launched by the link returns true if there is no AJAX support in the browser. Then the href attribute will be honored and everything works just like for non-JS browsers.
I’m willing to believe that the AJAX helpers already do this. Not 100% sure though. I’ll find out.
Does any one know any very well constructed on-line AJAX tutorials?
Awesome message. I think you overstate the POST vs GET advantages, though. Sure, POST messages are harder to type in manually (but not impossible, depending on the browser you’re using); but exactly as easy as GET messages from any HTTP API that’s likely to be used by any malicious webcrawler.
Otherwise, though, thanks for a great description of a very powerful technique.
RMX,
The reason for this technique isn’t to defend yourself from truly malicious, but stupid “crawlers” like Google Web Accelerator. GWA relies very heavily on the (false) assumption that GET requests never change the state of a web app so this article is mostly an advice on how to easily protect your Rails app from GWA, without sacrificing the accessibility for javascript-disabled.
Eli,
I think that horse is beaten to death many times already, as you can see from the linked bits in my article.
Highlights:
Hey thanks for this article Jarkko—I know a caught it a little late but I would have never thought to check for the type of request so that you can use the same links among the 3 choices. Very helpful!
Very nice. Question though, how do you make link_to_remote appear in a custom button (like you suggest)? I didn’t see that option in the docs.
Will,
You can create an image for that, or use some CSS rounded corners trickery. Remember that from the browsers rendering engine perspective link_to_remote produces just a normal anchor element so you can use and style it any way you want to.
This is a great tutorial and I also agree its good to show the different degredation options for those not using fancy AJAX.
I also concur you need techniques like this since things like Google accelerator could use one of your users session ids and escape typical authentication routes. There were reports of some people improperly setting up content management systems and as the bot crawled through the site it deleted all of their content…woops. Don’t have the URL handy but it managed to float up into the SEO forums.
As a side note, I would think this basic model might help other rails noobies to implement better options for people who don’t have javascript and get them thinking about web usability for a wider range of potential visitors.
Michael @ SEOG>
Wow! This is really helpful!
My only questions is what to do if there is a failure in the ‘post’ operation. There’s nowhere to report the error because the redirect resets the state AFAICT.
AJ,
That’s what
flashis for. However, the idea is that inside thepost?block you only redirect if the action was successful. If not, use flash to show an error message and render whatever page (e.g. a standard error page) you want to usingrender.