Pitfalls in RESTful "wizards"

Posted by Chad Pytel

Apr 25

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
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 responds_to_parent ?

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


Comments on this post

AW

Apr 25

AW said,

No session variable + unique wizard ID?

Pat Nakajima

Apr 25

Pat Nakajima said,

Phew. I was nervous from reading this headline that you were going to post one of those “Here’s one case where REST just doesn’t work” when it actually does. Kudos for sticking to good design.

Matthijs Langenberg

Apr 26

Matthijs Langenberg said,

I actually thought a wizard was used to guide a user through a complex information gathering process. If you need a bunch of data from a user, and it is too complex, so you decide to devide it into three parts, you should only save that data after step three. And you shouldn’t keep that in-between state server-side!

If you take, for example, the checkout procedure for a webshop. That could be done in one step (a page with a giant form). This is usally being split in several steps. In order to keep this thing RESTful, the posted data from the previous step should be included in the post data from the current step. This means a database record is only created if all data is available. If you write an external client for that API (e.g. POST to /orders), it can decide on its own how get the information from the user.

Matt Jankowski

Apr 26

Matt Jankowski said,

@Matthijs – in some apps maybe you could use that approach and either literally only “store” things in POST data passed from page to page, or in the session or something – but if you have a requirement that the wizard may be abandoned at anytime and then resumed later (including say, from a different browser on a different computer) – than you absolutely need to go server side and introduce state into the system so that only some things are “Complete” or “Submitted” or whatever.

You would most likely need to add state-conditional validations to the model to support this, as well.

Piers Cawley

Apr 28

Piers Cawley said,

A small thing, but that photo’s license is ‘attribution, non-commercial’. You’ve attributed it to Flickr rather than to its author, Michael Porter, which isn’t exactly sporting.

Chad Pytel

Apr 28

Chad Pytel said,

Thanks Piers, that was a mistake, and I fixed the attribution.

Steve Berryman

Apr 29

Steve Berryman said,

Great post there. This is the sort of thing i always forget to consider when dealing with complex forms and things.

Fadhli

Apr 30

Fadhli said,

The link to the plugin when leads to a broken link?

Fadhli

Apr 30

Fadhli said,

Anyway, what is the method

:team_has_registration_purchase?

I don’t quite get it.

Chad Pytel

Apr 30

Chad Pytel said,

@Fadhli, you’re right – I fixed the link to when.

team_has_registration_purchase? is a method on the purchases controller that checks to see whether the team we’re currently working on already has a purchase, returns true if it does, and false if it does now.

David Berube

May 02

David Berube said,

I find it curious the implied equation of “one resource equals one model.” As far as I know, that is a requirement of REST or of “good design”. For that matter, REST does not need to be backed by a database – static files are virtually always RESTful.

You may wish to consider a wizard as a resource in itself. For that matter, you may also wish to consider the atomicity of a wizard – users will typically assume a wizard makes no permanent change in state until it is finished, and will often restart wizards anew even if the option to continue an old session is present.

While a “this-is-not-a-finalized-record” flag is feasible, it is suboptimal, and conceivably other models could form relationships with non finalized records with nary a peep from the database. Of course, you could attempt to use either validations or database constraints to make this impossible, but would it not be better to structure your database so that each row is an actual conceptual entity and has no possibility of being a “ignore-this” record?

David Berube

David Berube

May 02

David Berube said,

Typo: Should have read “is not a requirement,” not “is a requirement”.

Take it easy,

Dave


Sorry, comments are closed for this article.

© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots