views:

81

answers:

3

As a long-time Ruby and Rails user, it never struck me until today to really think about the get-and-redirect pattern in Rails. The typical example of this would be calling a create() action, and then redirecting the user to a show() action to display the newly-created item:

class JournalEntries

  def index
    @entries = JournalEntry.all
  end

  def create
    @entry = JournalEntry.new( :name => "to-do list" )
    @entry.save
    redirect_to :action => "index"
  end
end

However, this has the inherent disadvantage that you are doubling your network traffic. This both slows down your users' site experience, as well as increasing your bandwidth charges.

So why not just do this instead:

  def create
    @entry = JournalEntry.new( :name => "to-do list" )
    @entry.save
    index

Same output, and no extra overhead required. But in addition to this, there is an even more substantial problem: redirect_to can only redirect using GET. This causes major problems for RESTful apps that use four different HTTP methods.

In my case, I wanted a user to be able to call /journals/8 and retrieve the Journal with that ID. If it wasn't found, I wanted to create a new, empty Journal object. In either case, the Journal object would then be sent to the caller.

Note that the create() method in RESTful Rails is routed from "POST /players". But since redirect_to (and the underlying HTTP redirect) can only send GET requests, it actually redirects to "GET /players", which is the index() method. This behavior is clearly wrong.

The only solution I could think of was to simply call create() instead of redirect_to() as in my example above. It seems to work fine.

Any thoughts on why redirect_to is preferred to calling actions directly?

A: 

I'm a Python/Django person, but the reasons for the redirect is language agnostic:

  1. If they do a page refresh they don't get that annoying "Resend data?" popup.
  2. This gives you a completely clean, RESTful URL for the page they are looking at. If you used POST it might not matter that much, but if GET was used for the update then you definitely want to get rid of any dangling params.
Peter Rowell
Just a comment on the second point. As I've understood it, one point with REST is to never do updates with GET, so that should never be an issue.
Jimmy Stenke
@Jimmy: Agreed, in proper REST it shouldn't happen.
Peter Rowell
so based on what you are both saying, it sounds like the problem is that what I'm trying to do is inherently non-restful - if one wants to return an object if it exists and create it if it does not, that would need to be two separate calls:GET /journal/1(if this returns no record, then the client calls):POST /journals/ [POST data: journal.id = 5 ]Is that correct? That is, there is no one-shot call in REST to do both as a single operation?
mrjake2
@mrjake2: Not really. Of course REST is not a religion, it is an architecture that is useful in a great many situations. Following it makes "explaining" your system to possible consumers (other programs) trivial. Your focus on minimizing connections may indicate there are other constraints in your system (e.g. comm resource consumption) that override the benefits of being "purely RESTful".
Peter Rowell
+1  A: 

If they do a page refresh they don't get that annoying "Resend data?" popup

It's not just that the popup is annoying (and makes no sense to most users) -- if the user clicks "yes, re-do the POST", he'll end up creating another Journal Entry (or whatever).

Also, it's annoying for the URL to read /posts/create instead of /posts since the user cannot copy / re-use it.

Horace Loeb
In my case that isn't actually true - the user is passing the ID of the object they want to create in the original request (it is coming from another application). So if they try to POST again, it will just return a copy of the object with that ID.
mrjake2
+1  A: 

The reason for it is as you point out. You redirect to a GET request, which is correct when it comes to REST (only do updates with POST/PUT, only get data with GET).

A redirect surely gives a little overhead with the redirect, but since no data is actually being sent between the browser and the server except for the POST data and the redirect (which is only sending the new url to the browser) I don't think that the issue of bandwith is of concern.

But on another point, you should not redirect to /journals (by calling redirect_to :index), you should redirect it to the newly created journal entry (by calling redirect_to @entry) which will work if you set up the routes correctly by, for instance map.resources :journals

Update:

I think, for creating the Journal when one doesn't exist, you should ask the user for more input. What is the reason for you to create the entry? The entry should have some text or some other input from the user, so I think from a REST (and rails) perspective you should actually redirect it to the new() method (with a GET request) where the user can input the additional information, that one will then POST the input and create the entry and after redirect to the newly created entry.

If you don't have any extra information that needs to put in, I'm not sure how to do it in a RESTful way, but I would probably have done it by putting the creation logic in a separate method that I would call from the create() and the show() method and then just continue with the show(), not redirecting at all, but also not calling the resource method.

Jimmy Stenke
I suppose I should have started with the second half of my post first - my primary problem isn't the network overhead, but rather with the actual methods being called. From the perspective of my controller (not thinking at all about how the methods are called), I want a call to Journal.show() to invoke a call to Journal.create() if the given Journal is not found, and then return the created object.As far as I can see, there is absolutely no way to do this using redirect_to in a REST application for the reason stated in my original post. Am I missing something?
mrjake2
Jimmy: In my case, there really isn't any user-supplied data other than an ID for the object (which is retrieved from a separate application). The create method just populates the object with a bunch of generic data and returns it; the caller then manipulates the object in a client application. Ultimately I think you have it exactly right in the last paragraph - I need a protected initialize_with_defaults() method in the controller that both show() and create() call.
mrjake2