Rails Routing and Facebook’s POSTs

If you have done any amount of Facebook development with Rails, you will quickly encounter the number one thing that has pissed off every Rails developer - Everything Is A POST!

With Facebooker’s pseudo-resource route generator, things are not so bad. It allows us declarations such as

  facebook_resources :entries
  facebook_resources :photos

in config/routes.rb which is nice and concise. However, the paths generated are so old school!

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries/create     # create
  GET    /entries/1/show     # show
  GET    /entries/1/edit     # edit
  POST   /entries/1/update   # update
  POST   /entries/1/destroy  # destroy

Yuck!

Rails taught us to love HTTP verbs, did it not? The above could be simplified if we took into account the HTTP request method, like so,

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries            # create
  GET    /entries/1          # show
  GET    /entries/1/edit     # edit
  PUT    /entries/1          # update
  DELETE /entries/1          # destroy

Unfortunately, you are stuck with just POST, buddy. Don’t even dream about your PUT and DELETE. Is there anything we can do? Fortunately, yes!

We Can Have Our RESTful Routes Back!

So, how do you solve this with Facebook? Facebook very recently added an additional request parameter to disambiguate between GET and POST via fb_sig_request_method. The actual requests from Facebook are still POSTs but we can make use of fb_sig_request_method as an added condition in our routes.

This is how you do it. Stick the following piece of code in lib and require it in a config/initializers file, or if you are using Facebooker as a plugin, modify vendor/plugins/facebooker/lib/facebooker/rails/routing.rb

  def facebook_resources(name_sym)
    name = name_sym.to_s

    with_options :controller => name, :conditions => { :method => :post } do |map|

      map.with_options :conditions => { :fb_sig_request_method => 'GET' } do |get|
        get.named_route("new_#{name.singularize}",  "#{name}/new",       :action => 'new')
        get.named_route(name,                       name,                :action => 'index')
        get.named_route(name.singularize,           "#{name}/:id",       :action => 'show', :id => /\d+/)
        get.named_route("edit_#{name.singularize}", "#{name}/:id/edit",  :action => 'edit', :id => /\d+/)
      end

      map.with_options :conditions => { :fb_sig_request_method => 'POST' } do |post|
        post.named_route("create_#{name.singularize}",  name,                  :action => 'create')
        post.named_route("update_#{name.singularize}",  "#{name}/:id",         :action => 'update',  :id => /\d+/)
        post.named_route("destroy_#{name.singularize}", "#{name}/:id/destroy", :action => 'destroy', :id => /\d+/)
      end

    end
  end

Pastie link

Notice the new :condition => { :fb_sig_request_method => 'GET'} and :condition => { :fb_sig_request_method => 'POST'}? That is the key we need to be able to overload a path like /entries/1 to call #show or #update depending on the condition. As a result, our routes now generate much nicer paths. Check it:

  GET    /entries            # index
  GET    /entries/new        # new
  POST   /entries            # create
  GET    /entries/1          # show
  GET    /entries/1/edit     # edit
  POST   /entries/1          # update
  POST   /entries/1/destroy  # destroy

A patch has been submitted to Facebooker. Hopefully the maintainers will see it fit to merge this in. As an added bonus, the patch also includes singleton resources via

  facebook_resource :entry

to generate the following routes:

  GET    /entry              # show
  GET    /entry/new          # new
  POST   /entry              # create
  GET    /entry/edit         # edit
  POST   /entry/update       # update
  POST   /entry/destroy      # destroy

Note that this is still very much a workaround until Facebook moves all the fb_sig metadata into the request headers and use real GETs and POSTs. But until then, happy programming!

Edge Rails now comes with protection from CSRF attacks which is implemented via a token that gets inserted in forms. This causes problems when developing Facebook applications on Rails if coupled with the Cookie Session Store which is the default Session Store in Rails 2.0 and Edge since Changeset 6184.

With Cookie Session Store, the token generator uses a salt generated from the cookie itself. I suspect Facebook modifies the cookie in between requests in ways that yield a different salt each time, causing the the token verifier to flag it as invalid.

Until I understand this problem in more detail, the current solution is to use ActiveRecord Session Store and set your own salt via the :secret option on protect_from_forgery. Rails will now successfully verify the token as legit.

Update: Looks like you can keep your Cookie Session Store. Just set the :secret manually as in the paragraph above and everything should be dandy.

Update: To make it clear, the exact exception you will hit is the ActionController::InvalidAuthenticityToken.

Update: Ignore the update that you can use Cookie Session Store with Facebook. You can’t. Until Facebook-the-giant-proxy forwards our cookies, any cookie that we read from the client will not from our own app but Facebook’s own.

We're Hiring
RSB is currently looking for Ruby On Rails developer and Web / Graphic Designer to join our amazing family.