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