Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find current abstract route in Rails middware

Rails version: '~> 4.2.7.1'

Spree version: '3.1.1'

TlDr: How do I get route as /api/products/:id or controller and action of that route in a middleware of Rails 4 application.

Details:

I am adding a middleware in my rails app which is similar to gem scout_statsd_rack. This adds following middleware to rails app to send metrics via statsd:

def call(env)
  (status, headers, body), response_time = call_with_timing(env)
  statsd.timing("#{env['REQUEST_PATH']}.response", response_time)
  statsd.increment("#{env['REQUEST_PATH']}.response_codes.#{status.to_s.gsub(/\d{2}$/,'xx')}")
  # Rack response
  [status, headers, body]
rescue Exception => exception
  statsd.increment("#{env['REQUEST_PATH']}.response_codes.5xx")
  raise
end

def call_with_timing(env)
  start = Time.now
  result = @app.call(env)
  [result, ((Time.now - start) * 1000).round]
end

What I want is to find current route in the middleware so that I can send metrics specific to each route.

I tried approach described here, which tells env['PATH_INFO'] can provide path, which it does, but it gives with URL params like this: /api/products/4 but what I want is /api/products/:id as my puropose is to track performance of /api/products/:id API.

env['REQUEST_PATH'] and env['REQUEST_URI'] also gives same response.

I tried answer provided here and here:

Rails.application.routes.router.recognize({"path_info" => env['PATH_INFO']})

or like this

Rails.application.routes.router.recognize(env['PATH_INFO'])

But it gave following error:

NoMethodError (undefined method path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:in
find_routes'
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:59:in recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
call'

This answer discusses request.original_url, but How do I access variable request, I think it should be same as env but not able to get route as want from this.

Edit #1

You can see the sample repo here, with code of rails middleware here, Setup of this can be done as stated in README and than this API can be hit: http://localhost:3000/api/v1/products/1.

Edit #2

I tried approach given by @MichałMłoźniak like following:

def call(env)
  (status, headers, body), response_time = call_with_timing(env)
  request = ActionDispatch::Request.new(env)
  request = Rack::Request.new("PATH_INFO" => env['REQUEST_PATH'], "REQUEST_METHOD" => env["REQUEST_METHOD"])
  Rails.application.routes.router.recognize(request) { |route, params|
    puts "I am here"
     puts params.inspect
     puts route.inspect
   }

But I got following response:

I am here 
{}
#<ActionDispatch::Journey::Route:0x007fa1833ac628 @name="spree", @app=#<ActionDispatch::Routing::Mapper::Constraints:0x007fa1833ace70 @dispatcher=false, @app=Spree::Core::Engine, @constraints=[]>, @path=#<ActionDispatch::Journey::Path::Pattern:0x007fa1833acc90 @spec=#<ActionDispatch::Journey::Nodes::Slash:0x007fa1833ad230 @left="/", @memo=nil>, @requirements={}, @separators="/.?", @anchored=false, @names=[], @optional_names=[], @required_names=[], @re=/\A\//, @offsets=[0]>, @constraints={:required_defaults=>[]}, @defaults={}, @required_defaults=nil, @required_parts=[], @parts=[], @decorated_ast=nil, @precedence=1, @path_formatter=#<ActionDispatch::Journey::Format:0x007fa1833ac588 @parts=["/"], @children=[], @parameters=[]>>

I have pushed the changes as well here.

like image 288
Saurabh Avatar asked Mar 09 '17 09:03

Saurabh


People also ask

How do I see routes in Rails?

Decoding the http request TIP: If you ever want to list all the routes of your application you can use rails routes on your terminal and if you want to list routes of a specific resource, you can use rails routes | grep hotel . This will list all the routes of Hotel.

How many types of routes are there in Rails?

Rails RESTful Design which creates seven routes all mapping to the user controller. Rails also allows you to define multiple resources in one line.

What is member routes in Rails?

Member routes can be defined for actions that are performed on a member of the resource . Let's take an example. Let's say we have a post resource and we need an ability to archive a post. To define routes to achive the functionality above, we will use member routes as given below.

How do routes work in Rails?

Rails routing is a two-way piece of machinery – rather as if you could turn trees into paper, and then turn paper back into trees. Specifically, it both connects incoming HTTP requests to the code in your application's controllers, and helps you generate URLs without having to hard-code them as strings.


1 Answers

You need to pass ActionDispatch::Request or Rack::Request to recognize method. Here is an example from another app:

main:0> req = Rack::Request.new("PATH_INFO" => "/customers/10", "REQUEST_METHOD" => "GET")
main:0> Rails.application.routes.router.recognize(req) { |route, params| puts params.inspect }; nil
{:controller=>"customers", :action=>"show", :id=>"10"}
=> nil

The same will work with ActionDispatch::Request. Inside middleware, you can easily create this object:

request = ActionDispatch::Request.new(env)

And if you need more information about recognized route, you can look into that route object that is yielded to block, by recognize method.

Update

The above solution will work for normal Rails routes, but since you only have spree engine mounted you need to use different class

request = ActionDispatch::Request.new(env)
Spree::Core::Engine.routes.router.recognize(request) { |route, params|
  puts params.inspect
}

I guess the best would be find a generic solution that works with any combination of normal routes and engines, but this will work in your case.

Update #2

For more general solution you need to look at the source of Rails router, which you can find in ActionDispatch module. Look at Routing and Journey modules. What I found out is that the returned route from recognize method can be tested if this is a dispatcher or not.

request = ActionDispatch::Request.new(env)
Rails.application.routes.router.recognize(req) do |route, params|
  if route.dispatcher?
    # if this is a dispatcher, params should have everything you need
    puts params
  else
    # you need to go deeper
    # route.app.app will be Spree::Core::Engine
    route.app.app.routes.router.recognize(request) do |route, params|
      puts params.inspect
    }
  end
end

This approach will work in case of your app, but will not be general. For example, if you have sidekiq installed, route.app.app will be Sidekiq::Web so it needs to be handled in different way. Basically to have general solution you need to handle all possible mountable engines that Rails router supports.

I guess it is better to build something that will cover all your cases in current application. So the thing to remember is that when initial request is recognized, the value of route yielded to black can be a dispatcher or not. If it is, you have normal Rails route, if not you need to recursive check.

like image 120
Michał Młoźniak Avatar answered Oct 20 '22 00:10

Michał Młoźniak