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¶m2=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
Related
I'm writing a middleware for my Rails applications, and I'm doing some sort of historic of actions made by the users. From the request I can get the path and the method, so I thought that should be a way to map it and get the method called by that combination.
This is the middleware so far:
class AccountabilityMiddleware
def initialize(app)
#app = app
end
def call(env)
dup._call env
end
def _call(env)
#status, #headers, #response = #app.call(env)
req = Rack::Request.new(env)
if !req.get? && req.path.starts_with?('/admin') && !req.path.starts_with?('/admin/login')
Accountability.create!(
user: env['warden'].user,
url: req.path,
method: req.request_method,
params: req.params,
method_name: ???????????????
response_status: #status,
response: #response.body,
)
end
[#status, #headers, #response]
end
end
For example, for a path /admin/my_model and method POST, I'd like to get "new_admin_my_model" -- A method already defined in the project (by ActiveAdmin)
I agree with #Konstantin that this is probably best left to the Rails router.
However, using info in a string to construct a new string isn't too hard if you know all the options.
Just spitballing, this is un-tested
method_constructor = '_admin_'
# just an example of some logic
pre =
case req.request_method
when POST
'new'
when GET
'show'
# and so on
end
method_constructor.prepend pre
method_constructor.append req.path.gsub('/admin/', '')
# method_constructor will now be a string like 'new_admin_my_model'
This logic obviously needs to be more complex than just considering the request_method in isolation, you'll probably need to map things out the same way Rails does.
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.
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 have a simple url shortener that base 62 encodes my Developer model's id number and returns something like this as a url:
http://example.com/d/dYbZ
I've mounted a rack app in my routes.rb file thusly:
match '/d/:token' => DeveloperRedirectApp
... and my simple Rack app looks like this:
class DeveloperRedirectApp
# no worky:
#def initialize(app)
# #app = app
#end
def self.call(env)
request = Rack::Request.new(env)
token = request.path_info.sub("/d/", "")
dev_id = token.b(62).to_s(10)
if dev = Developer.find_by_id(dev_id)
# developer_path also doesn't work since #app is not defined
location = #app.developer_path(dev)
else
# same here
location = #app.root_path
end
[301, {"Location" => location}, self]
end
def self.each(&block)
end
end
The problem is... apparently the initialize call is only sent a rails app instance if it's an actual middleware, not simply a rack app mounted in the routes file. A middleware doesn't make sense to me since this only needs to run if a url of the form /d/:token is requested, not on every request.
I'm just trying to do a simple base 62 decode, then redirect to the decoded developer id (if it exists, redirect to root_url otherwise). Is there a way to access the route helpers (ie, developer_path) or simply a better way to do this?
It might simply be easier to do it with a rails controller that does the redirect. eg:
routes:
get "/d/:token" => "developers#redirect"
in the developers controller:
def redirect
#magic goes here, use params[:token]
redirect_to some_url
end
I have a Rack application that looks like this:
class Foo
def initialize(app)
#app = app
end
def call(env)
env["hello"] = "world"
#app.call(env)
end
end
After hooking my Rack application into Rails, how do I get access to env["hello"] from within Rails?
Update: Thanks to Gaius for the answer. Rack and Rails let you store things for the duration of the request, or the duration of the session:
# in middleware
def call(env)
Rack::Request.new(env)["foo"] = "bar" # sticks around for one request
env["rack.session"] ||= {}
env["rack.session"]["hello"] = "world" # sticks around for duration of session
end
# in Rails
def index
if params["foo"] == "bar"
...
end
if session["hello"] == "world"
...
end
end
I'm pretty sure you can use the Rack::Request object for passing request-scope variables:
# middleware:
def call(env)
request = Rack::Request.new(env) # no matter how many times you do 'new' you always get the same object
request[:foo] = 'bar'
#app.call(env)
end
# Controller:
def index
if params[:foo] == 'bar'
...
end
end
Alternatively, you can get at that "env" object directly:
# middleware:
def call(env)
env['foo'] = 'bar'
#app.call(env)
end
# controller:
def index
if request.env['foo'] == 'bar'
...
end
end
Short answer: Use request.env or env inside a controller.
Long answer:
According to the Rails Guide on Rails controllers, ActionController provides a request method that you can use to access information about the current HTTP request your controller is responding to.
Upon further inspection of the docs for ActionController::Base#request, we see that it "Returns an ActionDispatch::Request instance that represents the current request."
If we look at the docs for ActionDispatch::Request, we see that it inherits from Rack::Request. Aha! Here we go.
Now, in case you're not familiar with the docs for Rack::Request, it's basically a wrapper around the Rack environment. So for most cases, you should just be able to use it as-is. If you really do want the raw environment hash though, you can get it with Rack::Request#env. So within the Rails controller, that would just be request.env.
Digging deeper:
After further examining the instance methods of ActionController::Base, I noticed there's not a whole lot there to look at. In particular, I noticed the params and session variables seem to be missing. So, I moved up one level to ActionController::Metal, which ActionController::Base inherits from.
In ActionController::Metal, I discovered a method env which had no documentation as to what it did - but I could guess. Turns out I was right. That variable was being assigned to request.env.
ActionController::Metal also contained the params method, which, according to the source, was set to request.parameters by default. As it turns out, request.parameters isn't from Rack::Request, but ActionDispatch::Http::Parameters, which is included by ActionDispatch::Request. This method is very similar to the Rack::Request#params method, except that altering it modifies a Rails-specific Rack environment variable (and therefore changes will remain persistent across instances of ActionDispatch::Request).
However, I still couldn't seem to find the session method. Turns out, it's not in the documentation at all. After searching the source code for ActionController::Metal, I finally found it on this line. That's right, it's just a shortcut for request.session.
To summarize:
In the controller...
Use request.env or env to get at the raw environment object
Use params to read Rack query strings and post data from the rack input stream. (E.g. Rack::Request#params)
Use session to access the value of rack.session in the rack environment
In the middleware...
Access properties of the environment the usual way through the environment hash
Access the Rails session through the rack.session property on the environment hash
Read params through Rack::Request#params
Update params through Rack::Request#update_param and Rack::Request#delete_param (as stated in the docs for Rack::Request#params)
Update params in a Rails specific way using ActionDispatch::Http::Parameters#params through ActionDispatch::Request