Wednesday, July 13, 2011

Resourceful != RESTful

Resourceful routing in Rails is certainly useful.  As is customary in Rails, you get a lot of stuff achieved with a very small amount of declarations.  And at first glance it looks like REST!  Yay!  Except... it will lead one slightly astray.  I've come across a few ways in which "resourceful" routing in Rails doesn't really follow REST principles, in ways that aren't merely theoretic but can affect intermediary behavior. 

Resourceful routing defines paths or routes for objects that the server developer would like to have manipulated with CRUD operations.  The developer can declare an 'invitation' to be a resource, and the routes to index all invitations, create a new invitation, download an invitation, update or delete the invitation, are automatically created.  But, problem the first:

  • Resourceful routing uses POST to create a new resource.  PUT is defined as creating a new resource in HTTP, and intermediaries can use that information-- but only if PUT is used.  If POST is used, an intermediary can't tell that a new resources was created. 

All routes share common error handling.  Route not found? Return 404!  But Rails puts the method in as part of the route definition.  Thus, problem the second:

  • Rails returns 404 Not Found if the client uses an unsupported method on a resource that exists.  So for example if I apply resourceful routing such that a resource can be downloaded with GET and updated with PUT, but don't define a POST variant route for that URL, then when the client tries to POST to that URL the server returns 404 because the route (with correct method) was not found.  In theory an intermediary could mark the resource as missing and delete its cached representation.  Instead, there's a perfectly good error to use in HTTP when a method is not supported on a URL, and that is 405 Method Not Allowed.
Rails has some magic to collect all the parameters on an incoming request and collect them in a params hash.  Thus, if my Web page takes a query parameter named "user_id", I just pull params["user_id"] and I've got it.  If my Web form sends the parameter inside the body instead, it's still there in params["user_id"]!  This lets server developers easily change their mind between having GET or POST used as the form submission method.  However, it creates two problems with intermediaries, linked to the way resources are named. 
  • URL parameters are mixed with body parameters. This may not cause problems for a POST, where the response typically isn't cachable anyway.  But it's a bad choice in for GET requests, where the URL containing parameters affects caching, while other parameters are unseen by intermediaries.  
  • Query parameters are treated the same as path elements.  Routes are defined as paths that can have parameters in them.  So if the routes file defines a route for "/v1/store/buy/:product/with_coins", the :product path element could be any string, and Rails will pass that string into the application just as if it were a URL query parameter.  However, caches are supposed to work differently if a URL has query parameters than if it does not, so treating them as the same is misleading the developer.
These kinds of things can be subtle to track down.  For example, it would be pretty hard to track down a bug where an intermediary was returning a cached response to a GET request when the Web server programmer had the response varying by parameters, and some client was sending those parameters in the body where the intermediary couldn't see them.  I guess the saving grace, if you call it that, is that real intermediary caching isn't as common as I had thought.  Even when intermediaries exist and could cache, Rails applications don't commonly seem to take advantage of that.

4 comments:

Anonymous said...

Show me, where PUT is defined as creating something...

Lisa said...

What I said about HTTP PUT is generally accepted, though I could be more precise.

To be precise, then:

"The PUT method requests that the enclosed entity be stored under the supplied Request-URI."

RFC 2616 also says "If a new resource is created, the origin server MUST inform the user agent via the 201 (Created) response."

If a PUT request is sent to a URI and a 201 response is returned, a cache can understand that the resource at that URI was created and has the PUT request content, roughly. If a POST request is used, the intermediary does not know what happened. So Rails' use of POST vs PUT has a potential effect on intermediaries getting less information.

Rails also does not use the 201 response by default (though you can make it do so like this, but part of my argument is that the defaults aren't RESTful.)

valakirka said...

Just a note: maybe I'm totally wrong but I found that older versions of Rails (for example 2.3.10) do not return a 404 status when using an unsupported method on an existent resource, at least for 'member' actions.

For example in this case:

map.resources :contents, :member => {:some_action => :post}

if you try to access to /contents/1/some_action via GET, a 405 Method Not Allowed status is returned.

Have this changed in newer versions of Rails? Maybe we should ask the members of the Rails core their reasons to do it ;)

Anonymous said...

If I'm not mistaken browsers are unable to submit method type PUT or DELETE. Rails is having to compensate for this feature defect by using POST to create.

Blog Archive

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License.