Rails: NoMethodError (Undefined method _url for _controller. I can't seem to respond_with json properly after my create. Why?

StackOverflow https://stackoverflow.com/questions/23582389

  •  19-07-2023
  •  | 
  •  

سؤال

  1. List item

My config/routes.rb file...

Rails.application.routes.draw do

  namespace :api, defaults: {format: 'json'} do
    namespace :v1 do
      resources :hotels do
        resources :rooms
      end
    end
  end

My app/controllers/api/v1/hotels_controller.rb

module Api
    module V1
        class HotelsController < ApplicationController
            respond_to :json
            skip_before_filter :verify_authenticity_token

            def index
                @hotels = Hotel.all
                respond_with ({hotels: @hotels}.as_json)
                #respond_with(@hotels)
            end

            def show
                @hotel = Hotel.find(params[:id])
                respond_with (@hotel)
            end

            def create
                @hotel = Hotel.new(user_params)
                if @hotel.save
                    respond_with (@hotel) #LINE 21
                end
            end

            private

                def user_params
                    params.require(:hotel).permit(:name, :rating)
                end
        end
    end
end

When I go to POST through Postman, my data saves just fine, but I get this NoMethodError. Why is this? The issue seems to be occurring at line 21, which is the respond_with(@hotel) line. Should it not just be responding with json ouput for the newly created hotel, via the show method?

(1.1ms)  COMMIT
Completed 500 Internal Server Error in 76ms

NoMethodError (undefined method `hotel_url' for #<Api::V1::HotelsController:0x0000010332df58>):
  app/controllers/api/v1/hotels_controller.rb:21:in `create'


  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_source.erb (1.0ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (1.7ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.4ms)
  Rendered /Users/.rvm/gems/ruby-2.0.0-p451@railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (31.5ms)
هل كانت مفيدة؟

المحلول

Because your route is in the API + v1 namespace, you actually need to redirect to the api_v1_hotel_url(@hotel) after you successfully create your resource. Of course, this is an API and there is no real redirecting, but the default Rails responder doesn't know that. It also doesn't know about your routing namespaces.

With just the default responder, you would have to do

respond_with :api, :v1, @hotel

So that Rails will build a URL that exists. Alternatively, you can create a custom responder that remove the :location option. Here is the default responder: http://api.rubyonrails.org/files/actionpack/lib/action_controller/metal/responder_rb.html

Reading through the source code for that class is very helpful in understanding respond_with. For example, you don't need to use if record.save before you use respond_with with this Responder. Rails will check if the record saved successfully for you and render a 422 with errors if it failed to save.

Anyway, you can see that the responder sets up a lot of variables in it's initializer:

def initialize(controller, resources, options={})
  @controller = controller
  @request = @controller.request
  @format = @controller.formats.first
  @resource = resources.last
  @resources = resources
  @options = options
  @action = options.delete(:action)
  @default_response = options.delete(:default_response)
end

If you subclassed this responder, you could make something like this:

class CustomResponder < ActionController::Responder
  def initialize(*)
    super
    @options[:location] = nil
  end
end

You can set a controller's responder using responder=:

class AnyController < ActionController::Base
  self.responder = CustomResponder

  # ...
end

To be clear, let me recap:

  1. When you use respond_with, Rails will try to infer what route to redirect to after a successful create. Imagine you had a web UI where you can create hotels. After a hotel is created, you will be redirected to that hotel's show page in the standard Rails flow. That is what Rails is trying to do here.
  2. Rails does not understand your route namespaces when inferring the route, so it attempts hotel_url - a route which does not exist!
  3. Adding symbols in front of the resource will allow Rails to infer the route correctly, in this case api_v1_hotel_url
  4. In an API, you can make a custom responder which just sets the inferred location to nil, since you don't actually need to redirect anywhere with a simple JSON response. Custom responders can also be useful in many other ways. Check out the source code.
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top