Pitfalls in RESTful Wizards

Chad Pytel

In an application we’re currently building, users go through a wizard to order teams. We implemented this as RESTful controllers - teams, purchases, and orders.

The relevant wizard steps are:

  1. teams/new
  2. teams/:id/purchases/new
  3. orders/new

When each step is submitted, the related create action is called and the user is redirected to the next new action.

Pitfall: the user hits the back button

On step 3, the user hits the back button. If they re-submit step 2, they will get a validation error because they cannot create two purchases for a team.

There’s a temptation here to stray from RESTful design. Don’t!

When the user hits the back button from step 3, we want to send them to purchases/edit for their newly-created object instead of purchases/new.

Solution: Force the browser to not cache

To ensure that the page isn’t cached by the browser and will always be re-fetched, we add a before_filter to the purchases controller which calls a private method on ApplicationController:

before_filter :no_cache, :only => [:new]

private

  def no_cache
    response.headers["Last-Modified"] = Time.now.httpdate
    response.headers["Expires"] = 0

    # HTTP 1.0
    response.headers["Pragma"] = "no-cache"

    # HTTP 1.1 'pre-check=0, post-check=0' (IE specific)
    response.headers["Cache-Control"] = 'no-store, no-cache, must-revalidate,
      max-age=0, pre-check=0, post-check=0'
  end

Next, if the team already has a purchase, we redirect to the edit action using another before filter on the purchases controller:

before_filter :redirect_to_edit, :only => [:new], :if => :team_has_registration_purchase?

#team_has_registration_purchase? is a method on the purchases controller. If the :if syntax is unfamiliar to you, its provided to us by our plugin, when.

Pitfall: ‘not caching’ only works in Firefox

To the best of our knowledge, setting the HTTP headers should have worked in all browsers, but it did not work in Safari or in IE 6 and 7. A little more research proved that any page with an iframe on it will never be cached, and will always be refetched.

Solution: iframe

So, we add this to views/purchases/new:

<iframe style="height:0px;width:0px;visibility:hidden" src="about:blank">
  this frame prevents back forward cache
</iframe>

This works. We now have cross-browser no-caching in a RESTful wizard.

The iframe hack feels a little dirty, though. Does anyone know a better way? Or, do we live with it, similar to using a hidden frame for Ajax file uploads with respondstoparent ?

This concept might be useful outside of these more complex, wizard type controller actions, and might come in handy if you had just a normal restful controller where you wanted the user to get the edit action instead of the new action if they use the back button. Have you done something like this before, or can you think of a different way to accomplish that?

photo courtesy of [Michael Porter] via Flickr