1. Oct 23rd, 2007

    Rails fixies: JSON as input, See Other and [].to_xml

    If you have a few moments, please read and tell me what you think would be the better option for each of these.

    1. JSON says

    Here’s a nice party trick:

    respond_to do |format|
      format.html
      format.xml  { render :xml=>@item }
      format.json { render :json=>@item }
    end

    When I’m feeling particularly evil, I walk over to someone still struggling to dual-render XML and HTML using indecipherable XSLT, ask them when JSON transformation will hit the roadmap, watch them squirm, jot these few lines on a piece of paper, put it on the table and quietly walk away.

    OK, not really. Actually I’m struggling doing it the other way around. Wouldn’t it make sense that I could GET a representation, change it, and PUT it back?

    For XML it turns out to be quite simple:

    def update
      Item.update params[:id], params['item']
    end

    Rails grabs the request body, an XML document with the document element item and stuffs the entire thing into a parameter with the same name.

    If you’re lazy you get rewarded. Let Rails do all the form building for you, and its magical helpers will use field names like item[name], item[count] and item[tags][]. Those are nicely parsed into a hash called item, so the above code will also work with hForm.

    But what about JSON? Test case please:

    curl -d "{ name: \"foo\", count: 2, tags: [ \"bar\" ] }" -H "Content-Type: application/json"

    The data is here, alright, and it’s the same structure we got back on the previous GET, but there’s nothing wrapping it up. JSON is naked. No XML document element name we can use to decide which parameter to place it in. Which means, one of these:

    Option 1. All JSON objects are called such:

    def update
      Item.update params[:id], params['item'] || params[:json]
    end

    This works nicely as long as you remember to always param[:json], which given my propensity to easily forget, is why I’m not particularly thrilled.

    Option 2. All non-named params are always called the same:

    def update
      Item.update params[:id], params['item'] || params[:data]
    end

    Looks the same, but is not. This option is forward looking and anticipates the possibility of some future technology we would want to use without having to go back and fix old code.

    Option 3. It always is data:

    def update
      Item.update params[:id], params[:data]
    end

    Even XML documents get called data. Everything gets called data. Which doesn’t work for query parameters or forms, but just for completeness I had to list this as well.

    Option 4. Name inference:

    def update
      Item.update params[:id], params['item']
    end

    This only works because the controller is called ItemsController, and the downcased singular name is item, and so we can infer what the JSON object should be called more often than not.

    I’m personally leaning towards a combination of both #1 and #4, so you can :json or :item it. What do you think?

    2. Are JSON objects hashes or arrays?

    All the use cases I have are for receiving records, whether represented as JSON objects or XML documents:

    <item>
      <name>foo</name>
    </item>
    
    { name: "foo" }

    But I do have code that renders collections, and contemplating the idea of receiving multiple records as inputs:

    <items type='array'>
      <item>
        <name>foo</name>
      </item>
    </items>
    
    [ { name: "foo" } ]

    The thinking at Rails core is that hashes are enough, although there’s no such restriction on XML input. Have you considered a use case for receiving arrays (XML and/or JSON)?

    3. redirect_to vs. see_other

    Here’s something you see quite often in Rails apps:

    def create
      article = Article.create(params[:article])
      redirect_to article
    end

    It seems like the right thing to do. It handles a POST request by creating the new resource, and then redirects the client to that resource (here, using polymorphic routes, a nice addition in Rails 2.o). Except it doesn’t, redirect_to will send back a 302 (Found) status code.

    The 302 status code tells the client that the resource is found in a different location. By which we mean the original resource, so the proper thing to do is head over to the new location and do the POST all over again. Not what the controller author had in mind!

    Per the HTTP specification, you would want to send back a 303 (See Other). Redirects (301, 302, 307) tell the client that the resource moved, and please can you try sending the same request over to the new location. 303 tells the client the request went through, got processed, please check the other location for the result.

    So how come this happens and the world doesn’t fall apart? Turns out browser developers got lazy, and some handled 302 and 303 the same way. People wrote CGI scripts by brushing through the HTTP spec and just testing out what works in the browser. And copying other people’s code. Eventually this bug got codified into the Undocumented HTTP Specification. So redirect_to doesn’t break browsers.

    When people write client applications that talk to a Web service, they either go lazy or go HTTP. Those that go lazy (send request, extract Location header) won’t break, but they’re losing an important HTTP feature: the ability to move resources and leave behind a forwarding address. Those that go full HTTP will either raise and error or attempt a second POST.

    I think we need to fix this, and get people to write services properly from day one.Option 1. Rails (2.0) gives you two options, both of which are examples for elegant use of hashed arguments:

    head :see_other, :location=>article_url(article)
    redirect_to article, :status=>:see_other

    Both return 303, as HTTP intended it to be, so not much to complain, except we all know how likely it is that people will take the extra step to add an obscure status code that so far hoards of developers ignored.

    Option 2. Temporary/permanent redirect and See Other are not the same thing, let’s make the difference known and introduce a see_other method:

    # Use this in response to an HTTP POST (or PUT), telling the client where the new resource is.
    # Works just like redirect_to, but sends back a 303 (See Other) status code.  Redirects should be used
    # to tell the client to repeat the same request on a different resource, and see_other when we want the
    # client to follow a POST (on this resource) with a GET (to the new resource).
    def see_other(options = {})
      if options.is_a?(Hash)
        redirect_to options.merge(:status=>:see_other)
      else
        redirect_to options, :status=>:see_other
      end
    end

    Option 3. Or sprinkle a bit of magic on redirect_to:

    def redirect_to(options = {}, response_status = {}) #:doc:
      if options.is_a?(Hash) && options[:status]
        status = options.delete(:status)
      elsif response_status[:status]
        status = response_status[:status]
      else
        status = request.post? ? 303 : 302
      end
      . . .

    Which one do you think is better?

    4. [1, 2, 3].to_xml

    This works:

    { 'foo'=>'bar' }.to_xml

    This throws with a violent exception:

    [ 'foo', 'bar' ].to_xml

    Any particular reason why it would be bad to XML-ize an array of primitive values?

    1. Oct 24th, 2007

      Gareth

      What about json option 5 – use a json container so that the json object is not “naked” e.g.
      { item : { name : “foo”, count : 2, tags : [...] } }
      then add the container properties directly into the params hash, so you can just use params[:item].

      Using a container like this has the added benefit of leaving room for adding more information into the json response/request such as errors.

    2. Oct 29th, 2007

      blweiner

      It’s interesting that Rails sends back 302s when you redirect; I was under the impression that 302 was informally deprecated in favor of 307. I’m curious why you’d want to send a 303 rather than a 201. While the 303 works–the request has succeeded, but instead of a representation here is a URI for it–it seems 201 is more appropriate. Am I missing something?

      Great work on everything you’ve done related to Rails. I’m busy incorporating your work with conditional GET into my app.

    3. Nov 12th, 2007

      Neville

      Hi Assaf,

      I’m also investigating sending JSON from the browser to my rails app, so you posting is very relevant to me. In your examples, you are using ‘curl’ to contruct the request which works just fine. However, I’m struggling to get prototype.js Ajax.request() to tell Rails that I’m sending application/json so that Rails automagically parses the incoming JSON into params as a ruby hash.

      I wonder if you might have a simple Prototype example handy which successfully sends JSON to Rails?

      TIA

      Neville

    4. Nov 30th, 2007

      Assaf

      Gareth, using a container object requires changing all your to_json to create a container object in the response, and does weird things when your JSON request looks like this:

      { item: { … }, _method: ‘delete’ }

      blweiner, I originally wanted one rendering, 303 works for both browser and programs, but perhaps it’s best to separate 201 (programmable) and 303 (browser).

      Neville, Ajax.Request accepts the option contentType, just set this to ‘application/json’. But you do need to create a JSON-encoded string, which unfortunately prototype.js doesn’t do for you.

    5. Sep 26th, 2008

      EP

      Sorry for raising this from the dead a year after the fact but I was investigating this issue and want to thank you for sharing your thoughts.

      I’d also like to add my feedback, since you asked :)

      In reference to #3, 303 See Other, I feel that altering the behaviour of redirect_to could be a little murky as you are changing something that is very well know. No Rails developer running your code and examining the output would have any idea why they are seeing a 303 status and not a 301. Even worse, because the redirect_to method is so ubiquitous would they even suspect that it wouldn’t output what it always outputs?

      Changing something which is well understood is never a good idea IMHO. I would rather see different flavors of redirect_to like see_other() or redirect_to_other() which make it obvious to the reader that what is being done is not redirect_to but something else, similar but different.

      Rather than redirect_to(…, :status => :see_other) which sends an empty response I prefer to use:

      def redirect_to_other(location)
      render(:inline => “……”, :layout => false, :status => :see_other, :location => location)
      end

      or similar.

      This implementation more closely satisfies the spec which states a 303 SHOULD include a hypertext note with a link to the new location in the response body. Both work, it’s optional, but what the hell. If you’re going to do it anyway why not.

      I’ve bunched my multi-flavored redirect_to_…() helpers as a module and include it in ApplicationController:

      redirect_permanently_to(…) # => 301
      redirect_to_other(…) # => 303
      redirect_temporarily_to(…) # => 307

      and use the stock redirect_to method for 302 redirects as written. This gets rid of the :status => ### ugliness too in the same way I use named_scope in my models.

      Cheers

    Your comment, here ⇓