Запросы PUT в рельсах не обновляют статус по вызовам response_with

Учитывая следующий контроллер в рельсах:

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

Я ожидал бы PUT запрос/accounts/update/для выполнения следующих

  • Если идентификатор существует и вызов update_attributes завершается успешно, доставьте сообщение об успешном завершении 204 (No Content). (У меня он установлен, чтобы вернуть @account, что было бы неплохо, но ничего страшного. 204 здесь отлично.)
  • Если идентификатор существует, но данные плохие, доставьте сообщение об ошибке 422 (Unprocessable Entity) вместе с xml/json, чтобы представить ошибку.
  • Если идентификатор не существует, доставьте сообщение об ошибке 404 (Not Found) вместе с xml/json, чтобы представить ошибку.

Что на самом деле происходит:

  • Поставьте 204 без тела.
  • Поставьте 204 без тела.
  • Поставьте 204 без тела.

Почему он игнорирует мой статус и мое тело? У меня была аналогичная настройка для GET запросов, которые работают просто отлично (правильный статус, правильное тело).

Пример CURL request (для идентификатора, который не существует):

PUT запрос

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 запрос

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"}

Ответ 1

В соответствии с этим обсуждением это довольно неинтуитивное поведение связано с желанием поддерживать совместимость с эшафотом.

В целом мы сохраняем ответчика той же самой реализации, что и строительные леса. Это позволяет нам сказать: заменить response_to на response_with и все будет работать точно так же.

- josevalim

У вас есть два варианта переопределения поведения по умолчанию.

A) Передайте блок response_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

К сожалению, нам приходится возвращаться к дублированию для каждого формата, но, похоже, это настоящая рекомендация до Rails 4.0; см. здесь.

Вы должны вернуть 200 - ОК, а не 204 - Нет содержимого, если вы возвращаете обновленный объект или ничего не возвращаете и имеете код клиента "GET 'обновленный объект.: местоположение не имеет смысла в контексте api, оно предназначено для перенаправления html-ответа.

B) Создайте собственный ответчик

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

Я не сделал этого сам, поэтому не могу привести пример, но, похоже, он все равно переполнен.

Откажитесь от Railscasts Episode: 224 для некоторого обсуждения response_with, включая настраиваемых респондентов.

Ответ 2

Вы видели класс ActionController:: Responder? Вот несколько способов подумать о

 # 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

и

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

Как вы можете видеть, api_behavior работает для сообщений и методов, но не для put. Если существующий ресурс изменен, коды ответов 200 (OK) или 204 (Нет содержимого) ДОЛЖНЫ быть отправлены, чтобы указать успешное завершение запроса.

head: no_content - это то, что вы получаете.

Итак, причина в том, что рельсы не понимают, что вы пытаетесь сделать. Rails считает, что при использовании response_with в этом случае нет ошибки (это не ошибка, которую вы просто не должны использовать так)

Я думаю, что respond_to - это то, что вам нужно.

Ответ 3

ИМХО, я бы попробовал это первым. .first! сделает рельсы испускать 404, если запись не найдена. В случае успеха отобразит 204. В случае ошибки при сохранении он будет извлекать ошибки из объекта ошибок модели.

class AccountsController < ApplicationController
  respond_to :json, :xml

  def update
    @account = Account.where(uuid: params[:id]).first!
    @account.update_attributes params[:account]
    respond_with @account, location: account_url(@account)
  end
end

Если сообщений о проверке модели недостаточно, тогда вам нужно будет условно испустить результат. Путь успеха будет работать так же, как и выше, и в случае неудачи вы сделаете то, что вам нужно.

class AccountsController < ApplicationController
  respond_to :json, :xml

  def update
    @account = Account.where(uuid: params[:id]).first!
    if @account.update_attributes params[:account]
      respond_with @account, location: account_url(@account)
    else
      respond_to error_hash do |format|
        format.json { render json: error_hash, status: :unprocessable_entity }
        format.xml { render xml: error_hash, status: :unprocessable_entity }
      end
    end
  end

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