How to find current abstract route in Rails middware - ruby-on-rails

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:infind_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.

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.

Related

How do I modify the request object before routing in Rails in a testable way?

So, I have a situation where I need to determine something about a request before it is dispatched to any of the routes. Currently, this is implemented using several constraints that all hit the database, and I want to reduce the database hit to one. Unfortunately, doing it inline in routes.rb doesn't work, because the local variables within routes.rb don't get refreshed between requests; so if I do:
# Database work occurs here, and is then used to create comparator lambdas.
request_determinator = RequestDeterminator.new(request)
constraint(request_determinator.lambda_for(:ninja_requests)) do
# ...
end
constraint(request_determinator.lambda_for(:pirate_requests)) do
# ...
end
This works great on the first request, but then subsequent requests get routed as whatever the first request was. (D'oh.)
My next thought was to write a Rack middleware to add the "determinator" to the env hash, but there are two problems with this: first, it doesn't seem to be sticking in the hash at all, and specs don't even go through the Rack middleware, so there's no way to really test it.
Is there a simple mechanism I'm overlooking where I can insert, say, a hook for ActionDispatch to add something to the request, or just to say to Rails routing: "Do this before routing?"
I am using Rails 3.2 and Ruby 1.9.
One way to do this would be to store your determinator on the request's env object (which you have since ActionDispatch::Request is a subclass of Rack::Request):
class RequestDeterminator
def initialize(request)
#request = request
end
def self.for_request(request)
request.env[:__determinator] ||= new(request)
end
def ninja?
query_db
# Verify ninjaness with #request
end
def pirate?
query_db
# Verify piratacity with #request
end
def query_db
#result ||= begin
# Some DB lookup here
end
end
end
constraint lambda{|req| RequestDeterminator.for_request(req).ninja? } do
# Routes
end
constraint lambda{|req| RequestDeterminator.for_request(req).pirate? } do
# Routes
end
That way, you just instantiate a single determinator which caches your DB request across constraint checks.
if you really want to intercept the request,try rack as it is the first one to handle request in any Rails app...refer http://railscasts.com/episodes/151-rack-middleware to understand how rack works....
hope it helps.

Handling a POST with an "action" parameter in Rails

My API is handling a callback from an external source which sends a POST that contains an action parameter. This parameter has nothing to do with the rails action, it just happens to be named the same:
param1=value1&action=example&param2=value2
When this hits rails, rails overrides the parameter to be the name of the action (in this case create)... so I get:
{ action: 'create', param1: 'value1', params2: 'value2' }
How can I access the original action parameter in a clean way? Currently I have to parse the raw_post:
Rack::Utils.parse_query(request.raw_post)["action"]
Which is ugly... anything better?
You can create a Rack middleware that intercepts requests that have action in the params and renames them before passing forward.
I believe it would look something like:
class ActionParamRenamer
def initialize(app)
#app = app
end
def call(env)
# you might want to only do this for certain paths
if env["rack.request.form_hash"] && env["rack.request.form_hash"]["action"]
env["rack.request.form_hash"]["action_param"] = env["rack.request.form_hash"].delete("action")
end
if env["rack.request.form_vars"] && env["rack.request.form_vars"].match(/[\?&]action\=/)
env["rack.request.form_vars"].gsub!(/([\?&])action\=/, /\1action_param=/)
end
#app.call(env)
end
end
and I guess you would add this to your config/initializers/action_renamer.rb or however that works in Rails nowadays :
Rails::Initializer.run do |config|
config.middleware.use "ActionParamRenamer"
end

How can you universally override the get and post methods in RSpec?

I'd like to override the get and post methods in RSpec.
I want to do this in order to deal with subdomains in my tests. As far as I can tell, the only way to deal with subdomains is to alter the #request object before each call. I could do this before each and every test but that's going to lead to some really messy code.
In an effort to keep things DRY I've tried using a config.before(:each) method in spec_helper.rb however this doesn't seem to be run in the same scope as the test and doesn't have access to #request.
My next bsest approach is therefore to overrride get and post which are in the correct scope.
def get *args
#request.host = #required_domain if #required_domain
super *args
end
I can include this code in the top of each spec file but I'd rather set it universally. If I set it in spec_helper.rb though it does not get called.
Where can I set this to override the default get method?
however this doesn't seem to be run in the same scope as the test.
That's not quite right - it's run in the same scope, but before #request is configured, so it has no effect.
Try this:
module RequestExtensions
def get(*)
#request.host = #required_domain if #required_domain
super
end
end
RSpec.configure do |c|
c.include RequestExtensions, :type => :controller
end
HTH,
David
I just ran into an issue which routed me to this question. The accepted solution guided me to the more effective implementation as of rack-test 0.6.3.
I manually created ./spec/helpers/rspec_http_request_override_helper.rb
module RspecHttpRequestsOverrideHelper
def get(uri, params = {}, env = {}, &block)
super(uri, params, set_json_headers(env), &block)
end
def post(uri, params = {}, env = {}, &block)
super(uri, convert_to_json(params), set_json_headers(env), &block)
end
def put(uri, params = {}, env = {}, &block)
super(uri, convert_to_json(params), set_json_headers(env), &block)
end
def delete(uri, params = {}, env = {}, &block)
super(uri, convert_to_json(params), set_json_headers(env), &block)
end
# override other HTTP methods if necessary
private
def set_json_headers(env={})
env.merge({'ACCEPT' => "application/json", 'CONTENT_TYPE' => 'application/json'}) unless env.nil?
end
def convert_to_json(params={})
params.to_json unless params.nil?
end
end
Then I added the below to my spec_helper.rb
# require assuming project root is loaded into ruby's class paths
require './spec/helpers/rspec_http_request_override_helper'
RSpec.configure do |config|
config.include RspecHttpRequestsOverrideHelper
# Other settings
end
And that was it!
Note: The get method above doesn't convert the params value to json intentionally. Param values are encoded into the query string and then sent. Not as part of the HTTP body in the request, even though the GET http method supports sending a body; see here for more details.
My issue was that the rspec test helpers for an API I am building were converting boolean types to string types when sending the request to the API. Turns out when you don't specify a content-type header for json the data is passed as multipart/form-data or x-www-form-urlencoded depending on the HTTP method; see here for more details. This was converting my special data types, which are valid in json like integers and booleans, into strings. And effectively needing me to convert them on the API's end. It wasn't until I added validation for the input into my API that this was exposed. Yay for validations and tests!
Now, I needed to effectively apply a content-type header to all my requests and convert the params to json when sending the requests; I was calling the http methods with the params value being a ruby hash. I have over 200 tests so going in an manually changing them all would not have been an optimal solution. So I implemented the below solution. Which works very well.
I decided to follow the same method definition as rack-test was and then I could effectively call super after editing the requests.
My failing tests now started passing and my previous tests where none the wiser.
Hopefully this helps others who run into a similar issue.
The question betrays incorrect assumptions. You shouldn't be writing controller specs in the first place. This should all be done with Cucumber -- and in that case, you can just specify particular URLs, so the problem goes away.

Async_Sinatra in Rails: async actions can't write to shared session

I have a Sinatra class in a Rails project. It uses eventmachine and async_sinatra to make asynchronous calls to external sites. I'd like to write to a session object (ideally, the same one that Rails is using), but so far I can only:
write to a separate session object from Rails' (by default, Sinatra names its session something different from Rails)
write to the same session for synchronous calls only
When I make asynchronous calls, sessions written in the async_sinatra code don't get pushed out to the client machine. I suspect one of two things is happening:
The header has already been sent to the client and the local variable storing the session (in Sinatra) will be flushed out at the end of the action. The client would never see a request from the server to save this data to a cookie.
The header is being sent to the client, but Rails immediate sends another, instructing the client to write to the cookie what Rails has stored in its session variable, overwriting what Sinatra wrote.
Either way, I'd like to just get simple session functionality in both Sinatra and Rails. An explanation of what I'm doing wrong would also be nice :)
A full working copy of the code is on github, but I believe the problem is specifically in this code:
class ExternalCall < Sinatra::Base
use ActionDispatch::Session::CookieStore
register Sinatra::Async
get '/sinatra/local' do
session[:demo] = "sinatra can write to Rails' session"
end
aget '/sinatra/goog' do
session[:async_call]="async sinatra calls cannot write to Rails' session"
make_async_req :get, "http://www.google.com/" do |http_callback|
if http_callback
session[:em_callback] = "this also isn't saving for me"
else
headers 'Status' => '422'
end
async_schedule { redirect '/' }
end
end
helpers do
def make_async_req(method, host, opts={}, &block)
opts[:head] = { 'Accept' => 'text/html', 'Connection' => 'keep-alive' }
http = EM::HttpRequest.new(host)
http = http.send(method, {:head => opts[:head], :body => {}, :query => {}})
http.callback &block
end
end
end
EDIT 7/15:
Changed code on Github to include Async-Rack. Async-sinatra can write to sessions when they are not shared with Rails. Compare the master and segmented_sessions branches for behavior difference. (Or on the master branch, change use ActionDispatch::Session::CookieStore to enable :sessions)
This is because async_sinatra uses throw :async by default, effectively skipping the session middleware logic for storing stuff. You could override async_response like that:
helpers do
def async_response
[-1, {}, []]
end
end

How do you log the URL ActiveResource uses?

Rails ActiveResource is awesome ... except for one thing: as far as I can tell, there is no way to see what URL it is using behind the scenes. For instance, let's say I have an ActiveResource called Issue, for a webservice at myIssues.com/issues.xml. If I do:
Issue.find(:all, :params => {:page => 2})
I would expect that ActiveResource would make a call to:
myIssues.com/issues.xml?page=2
... but I don't actually know that. For all I know, ActiveResource could have decided it doesn't like the word "page", so it's actually using:
myIssues.com/issues.xml?mod_page=2
This makes debugging difficult. Right now I've got a situation where, if I go to the URL I think ActiveResource is using, it works just fine. However, when I actually use ActiveResource, it doesn't work. Seeing the URL it's GETing would be immensely helpful in this, so ...
Does anyone know a way to log (or otherwise output; if there's some resource.url method that would work great too) the URL(s) that ActiveResource uses to do its thing?
If you add the following line to your environment.rb file, it will at least log the requests so you know that URLs ActiveResource is hitting:
ActiveResource::Base.logger = ActiveRecord::Base.logger
I'm still searching for a better solution that shows me the response and the data posted to update calls, but at least this is a step in the right direction. I'm really not sure why ActiveResource has a separate logger to start with, but that's another matter.
I just ran into this same exact issue, and came across this post as I was looking for answers. What I did find, that proved useful, is the collection_path method on ActiveResource::Base. So for example, let's say you have the following resource:
class UserPost < ActiveResource::Base
self.site = "http://someApp.com/user/:user_id"
self.element_name = "post"
If you go to the rails console, here are some examples of the output:
>> UserPost.collection_path
"/user//post"
>> UserPost.collection_path(:user_id => 5)
"/user/5/post
This should provide you with exactly what you need to determine how ActiveResource is translating your request into a URL.
To get detail login for ActiveResource have to patch the request method inside the gem(method.
place bellow files inside config/initializers you will get http method, path, request body, request hedaers
response body and header is already there if you need. doc
config/initializers/activeresource_patch.rb
module ActiveResource
class Connection
private
def request(method, path, *arguments)
result = ActiveSupport::Notifications.instrument("request.active_resource") do |payload|
payload[:method] = method
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
payload[:request_path] = path
payload[:request_body] = arguments[0]
payload[:request_headers] = arguments[1]
payload[:result] = http.send(method, path, *arguments)
end
handle_response(result)
rescue Timeout::Error => e
raise TimeoutError.new(e.message)
rescue OpenSSL::SSL::SSLError => e
raise SSLError.new(e.message)
end
end
end
config/initializers/activeresource_logger.rb
Rails.application.configure do
def activeresource_logger
#activeresource_logger ||= Logger.new("#{Rails.root}/log/activeresource_logger.log")
end
ActiveSupport::Notifications.subscribe('request.active_resource') do |name, start, finish, id, payload|
if Rails.env.development?
activeresource_logger.info("====================== #{start} : #{payload[:method].upcase} ======================")
activeresource_logger.info("PATH: #{payload[:request_path]}")
activeresource_logger.info("BODY: #{payload[:request_body]}")
activeresource_logger.info("HEADERS: #{payload[:request_headers]}")
# activeresource_logger.info("STATUS_CODE: #{payload[:result].code}")
# activeresource_logger.info("RESPONSE_BODY: #{payload[:result].body}")
end
end
end

Resources