Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PUT requests in rails don't update the status on respond_with calls

Given the following controller in rails:

class AccountsController < ApplicationController
    respond_to :json, :xml
    def update
        @account = Account.where(uuid: params[:id]).first
        unless @account.nil?
            if @account.update_attributes params[:account]
                respond_with @account, location: account_url(@account)
            else
                respond_with error_hash, status: :unprocessable_entity, root: :error, location: api_account_url(@account)
            end
        else
            respond_with error_hash, status: :not_found, root: :error, location: accounts_url
        end
    end

    def error_hash
        { :example => "Example for this question", :parameter => 42 }
    end
end

I would expect a PUT request to /accounts/update/ to do the following

  1. If the id exists, and the update_attributes call succeeds, deliver a 204 (No Content) success message. (I have it set to return @account, which would be nice, but no big deal. 204 is fine here.)
  2. If the id exists, but the data is bad, deliver a 422 (Unprocessable Entity) error message, along with the xml/json to represent the error.
  3. If the id does not exist, deliver a 404 (Not Found) error message, along with the xml/json to represent the error.

What actually happens is:

  1. Deliver a 204 with no body.
  2. Deliver a 204 with no body.
  3. Deliver a 204 with no body.

Why is it that it ignores both my status and my body? I've had a similar setup for GET requests that work out just fine (correct status, correct body).

Example CURL request (for an ID that does not exist):

PUT request

curl -i --header "Accept: application/xml" --header "Content-type: application/json" -X PUT -d '{"name": "whoop"}' http://localhost:3000/api/accounts/3d2cc5d0653911e2aaadc82a14fffee9
HTTP/1.1 204 No Content 
Location: http://localhost:3000/api/accounts
X-Ua-Compatible: IE=Edge
Cache-Control: no-cache
X-Request-Id: bf0a02f452fbace65576aab6d2bd7c1e
X-Runtime: 0.029193
Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-01-15)
Date: Thu, 24 Jan 2013 08:01:31 GMT
Connection: close
Set-Cookie: _bankshare_session=BAh7BkkiD3Nlc3Npb25faWQGOgZFRkkiJWFmNmI2MmU0MzViMmE3N2YzMDIzOTdjMDJmZDhiMzEwBjsAVA%3D%3D--133e394eb760a7fce07f1fd51349dc46c2d51626; path=/; HttpOnly

GET request

curl -i --header "Accept: application/json" --header "Content-type: application/json" -X GET http://localhost:3000/api/accounts/3d2cc5d0653911e2aaadc82a14fffee9
HTTP/1.1 404 Not Found 
Content-Type: application/json; charset=utf-8
X-Ua-Compatible: IE=Edge
Cache-Control: no-cache
X-Request-Id: 9cc0d1cdfb27bb86a206cbc38cd75473
X-Runtime: 0.005118
Server: WEBrick/1.3.1 (Ruby/1.9.3/2013-01-15)
Date: Thu, 24 Jan 2013 08:19:45 GMT
Content-Length: 116
Connection: Keep-Alive

{"friendly-status":"not-found","status":404,"message":"No account with id '3d2cc5d0653911e2aaadc82a14fffee9' found"}
like image 716
ashays Avatar asked Jan 24 '13 08:01

ashays


2 Answers

According to this discussion this rather non intuitive behavior is due to the desire to maintain compatibility with the scaffold.

In general we keep the responder the same implementation as the scaffold. This allows us to say: replace respond_to by respond_with and everything will work exactly the same.

-- josevalim

You have two choices to override the default behavior.

A) Pass a block to respond_with

unless @account.nil?
  if @account.update_attributes params[:account]
    respond_with @account do |format|
      format.json { render json: @account.to_json, status: :ok }  
      format.xml  { render xml: @account.to_xml, status: :ok }
    end
  else
    respond_with error_hash do |format|
      format.json { render json: error_hash.to_json(root: :error), status: :unprocessable_entity }
      format.xml { render xml: error_hash.to_xml(root: :error), status: :unprocessable_entity }
    end
  end
else
  respond_with error_hash do |format|
    format.json { render json: error_hash.to_json(root: :error), status: :not_found }
    format.xml { render xml: error_hash.to_xml(root: :error), status: :not_found }
  end
end

It's unfortunate that we have to return to duplication for each format, but that seems to be the current recommendation up to Rails 4.0 so far; see here.

You should return 200 - OK, not 204 - No Content if you are returning the updated object, or don't return anything and have your client code 'GET' the updated object. :location is not meaningful in an api context, it's for redirecting an html response.

B) Create a custom responder

respond_with @account, status: :ok, responder: MyResponder

I've not done this myself so I can't give an example, but it seems like overkill here anyway.

Check out Railscasts Episode:224 for some discussion of respond_with including custom responders.

like image 68
Joshua Kolden Avatar answered Nov 08 '22 20:11

Joshua Kolden


Did you see ActionController::Responder class ? Here are some methods to think about

 # All other formats follow the procedure below. First we try to render a
    # template, if the template is not available, we verify if the resource
    # responds to :to_format and display it.
    #
    def to_format
      if get? || !has_errors? || response_overridden?
        default_render
      else
        display_errors
      end
    rescue ActionView::MissingTemplate => e
      api_behavior(e)
    end

and

def api_behavior(error)
      raise error unless resourceful?

      if get?
        display resource
      elsif post?
        display resource, :status => :created, :location => api_location
      else
        head :no_content
      end
    end

As you can see api_behavior works for post and get methods but not for put . If an existing resource is modified, either the 200 (OK) or 204 (No Content) response codes SHOULD be sent to indicate successful completion of the request.

head :no_content is what you get.

So reason of this is that rails doesn't understand what are you trying to do. Rails thinks there is no error when you use respond_with in this case.(it's not a bug you just shouldn't use it that way)

I think respond_to is what you need.

like image 3
Fivell Avatar answered Nov 08 '22 19:11

Fivell