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
Related
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.
In my rails app a lib class is used to recieve emails. The email reciever class parses the email and post it to a application controller using a HTTP response. The code for this is as shown :
uri = 'http://localhost:3000/incoming_mail'
body = {'from'=> from, 'to' => to, 'subject' => subject, 'message' => message}
response = Net::HTTP::post_form(URI.parse(uri), body)
The problem is I don't want to specify the complete URL. Is there any way to use 'incoming_mail_path' instead of 'localhost:3000/incoming_mail' ?
I tried to add :
include Rails.application.routes.url_helpers
But this is not working and gives the following error :
<class:EmailReceiver>': uninitialized constant EmailReceiver::Rails (NameError)
Can anyone please suggest a solution for this.
I am posting the entire class here(Updated the class with include statement):
require 'mail'
require 'net/https'
require 'net/http'
require 'uri'
class EmailReceiver
include Rails.application.routes.url_helpers
attr_accessor :url
def initialize
end
def submit(content)
mail = Mail.read_from_string(content)
body = mail.body.decoded
from = mail.from.first
to = mail.to.first
subject = mail.subject
if mail.multipart?
part = mail.parts.select { |p| p.content_type =~ /text\/plain/ }.first rescue nil
unless part.nil?
message = part.body.decoded
end
else
message = mail.decoded
end
unless message.nil?
uri = incoming_mail_url
#uri = 'http://localhost:3000/incoming_mail'
body = {'from'=> from, 'to' => to, 'subject' => subject, 'message' => message}
response = Net::HTTP::post_form(URI.parse(uri), body)
end
end
end
handler = EmailReceiver.new
handler.submit(STDIN.read)
Debugging:
After reading your comments, i figured out that you are running it as a ruby script which does't even recognize Rails.
Before figuring out how to include all the requirements to the file. I tried to run the file through rails environment (while the server was already running) by:
cat sample.email | bundle exec rails runner "eval(File.read 'lib/email_receiver.rb')"
I got the error for the incoming_mail_url:
Missing host to link to! Please provide the :host parameter,
set default_url_options[:host], or set :only_path to true (ArgumentError)
While the incoming_mail_path successfully executed /incoming_mail (which is not what you need).
Conclusion:
This means that what ever you do, as long as you don't run the file from the server (ex: initializers) then the host will never exist.
When you run this module from your server, its going to recogize the route through the url_helpers that you have included.
Alternative suggestion:
Griddler gem is a Rails engine that provides an endpoint for services that convert incoming emails to HTTP POST requests. It parses these POSTs and hands off a built email object to a class implemented by you.
To solve your immediate problem you may want to try to unscope your include statement. Right now it seems that interpreter thinks that "Rails" is a class within the EmailReceiver namespace. if you Add :: to the include statement it should reference "Rails" at the top level which is what you want.
It should work, but there maybe something else wrong in your setup that is causing you to have use this otherwise unnecessary workaround
Edit
What I meant by add "::" just to clarify
include ::Rails.application.routes.url_helpers
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.
I am new to Ruby and Rails. I am playing with Rack, trying to get a basic understanding of this peice of the Rails puzzle, following along with Rob Conery in his Tekpub/Rails 3 tutorial vid.
Unfortunately, the version of Rack used in the vid has become long in the tooth, and I think something has changed in between the video release and now (as have some things in Ruby between 1.8.x and 1.9.x). Even more unfortunately, I don't yet know enough about Ruby or Rack to know how to figure out what I need to do differently. The version of Rack used in the video is 1.1. The version on my machine is 1.4.5.
Silly example code:
class EnvironmentOutput
def intialize(app)
#app = app
end
def call(env)
out = ""
unless(#app.nil?)
response = #app.call(env)[1]
out+=response
end
env.keys.each {|key| out+="<li>#{key}=#{env[key]}"}
["200", {"Content-Type" => "text/html"}, [out]]
end
end
class MyApp
def call(env)
["200", {"Content-Type" => "text/html"}, ["<h1>Hello There</h1>"]]
end
end
# My understanding is that this should work:
use EnvironmentOutput
run MyApp.new
When I run this, I get the following:
ArgumentError: wrong number of arguments(1 for 0)
This is where the first in a series of errors occurs (line 82 in the rack Builder class):
def use(middleware, *args, &block)
if #map
mapping, #map = #map, nil
#use << proc { |app| generate_map app, mapping }
end
# error occurs HERE:
#use << proc { |app| middleware.new(app, *args, &block) }
end
Obviously, I am passing something incorrectly. Sadly, I don't yet know enough to figure out what it is I am doing wrong. I have searched on Google and here on SO, but I suspect I also don't have quite a strong enough grasp on the Ruby/Rails/Rack relationship to know what to ask and get a reasonably helpful result (or, if I AM, then I don't yet recognize it).
Does anyone know what I am doing wrong here?
UPDATE: Thanks to the selected answer, I realize it was a typo. Next issue is an array-to-string conversion problem in the same code, but will post as new question.
It’s just a typo:
def intialize(app)
should be
def initialize(app)
(you’ve missed an i).
Since you don’t provide an initialize method, Ruby tries to use the default, argument-less one, but since Rack passes an argument (the app) you get the ArgumentError.
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.