Rails 3.2.X logging mechanism has improved tremendously.
Still, I'm looking for a way to add a prefix of 'elapsed time since request has started' (in milliseconds or a timestamp) before each log item. (Request is identified by uuid)
I figure that I may have to write my own Rack middleware for this, but maybe there's a simpler solution out of the box?
UPDATE
I built my own Rack middleware:
class RequestElapsedTime
def initialize(app)
#app = app
end
def call(env)
# Set request start time so it can be used as part of the 'request' context.
env['REQUEST_START_TIME'] = Time.now
# Call next middleware component
status, headers, response = #app.call(env)
# return a valid rack response
[status, headers, response]
end
end
And added the following to application.rb:
elapsed_time = lambda do |req|
req_start_time = req.env['REQUEST_START_TIME']
return unless req_start_time
(Time.now - req_start_time) * 1000.0
end
# Add request UUID to logs
config.log_tags = [:uuid, elapsed_time]
Unfortunately, for some reason, this outputs the same elapsed time for different logs items in the same request, which leads me to think that it might have to do with the fact that lambda is being evaluated for multiple items at once (Buffered Logging?)...
any idea how to solve this?
Related
I have no need for this feature and am only asking out of curiosity.
I'm aware that middlewares run before to each request.
Is it, however, reasonable to expect middleware to run after each request?
If that's the case, how can we go about doing it?
If not, how does the logger middleware report the response to the request?
in Rails, middlewares are arranged in a stack (you could consider this stack is a pipeline), the request and the response go throw the stack in 2 opposite directions.
rails middleware stack
$ rails middleware
request # ... ^
| use Rack::ETag |
| use Rack::TempfileReaper |
| use Warden::Manager |
| run Bookstore::Application.routes response
V
In order to those middlewares in stack link together, the higher middlewares will call recursive the lower middlewares, let see some code to understand how it works, suppose we have 2 middlewares m1 and m2 in which m1 is arranged right above m2 in the stack, then the flow the request and the response go throw m1, m2 as below (steps in order [1], [2], ...):
class M1
def initialize(app)
#app = app
end
def call(env) # [1] higher middleware call
# [2] before call recursive
# you could get request
request = ActionDispatch::Request.new env
# log request , or something else ...
status, headers, body = \ # [9] get response from M2
#app.call(env) # [3] call recursive M2
# log response, or do something else ...
[status, headers, body] # [10] return response to higher middleware
end
end
class M2
def initialize(app)
#app = app
end
def call(env) # [4] M1 call
# [5] before call recursive lower middleware
# same as above
status, headers, body = \ # [7] get response from lower middlewares
#app.call(env) # [6] call recursive lower middleware
# log response, or do something else ...
[status, headers, body] # [8] return response to M1 above
end
end
And the lowest middleware is Rails app, so the call stack look like a chain
call( ... call( ... call( ... rails_app.call() ))..)
so you could change app behavior (or how your app handle request/response) by adding/inserting_before(after)/removing nodes in this chain, very flexible !!!
Recently I upgraded from Rails 6.0.3.4 to 6.1.3. ActiveStorage deprecated combine_options, which I cleared from my app. All fresh request work as expected.
Internet Bots (Facebook, Google, ...) cache urls to images hosted on a website (like mine). According to my Rollbar records they request these a couple of times a day.
The cached URL's that should load ActiveStorage attachments include an old variation_key in the URL. When the blob wants to load using the decoded variation_key, I see that combine_options is still present. This throws a 500 Internal Server Error with ArgumentError (Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command.):.
Is there any way I can stop these errors from showing up?
Rails version: 6.1.3.
Ruby version: 2.7.2p137
I have resolved this issue using some middleware. This will intercept all incoming requests, scan if they are ActiveStorage urls, find the ones with the deprecated combine_options and just return 404 not found. This code will also raise an error is the current environment is development, this way I don't accidentally introduce the deprecated code again.
For those of you who might have the same problem, here's the code.
application.rb
require_relative '../lib/stopper'
config.middleware.use ::Stopper
lib/stopper.rb
class Stopper
def initialize(app)
#app = app
end
def call(env)
req = Rack::Request.new(env)
path = req.path
if problematic_active_storage_url?(path)
if ENV["RACK_ENV"] == 'development'
raise "Problematic route, includes deprecated combine_options"
end
[404, {}, ['not found']]
else
#app.call(env)
end
end
def problematic_active_storage_url?(path)
if active_storage_path?(path) && !valid_variation_key?(variation_key_from_path(path))
return true
end
false
end
def active_storage_path?(path)
path.start_with?("/rails/active_storage/representations/")
end
def variation_key_from_path(path)
if path.start_with?("/rails/active_storage/representations/redirect/")
path.split('/')[6]
elsif path.start_with?("/rails/active_storage/representations/")
path.split('/')[5]
end
end
def valid_variation_key?(var_key)
if decoded_variation = ActiveStorage::Variation.decode(var_key)
if transformations = decoded_variation.transformations
if transformations[:combine_options].present?
return false
end
end
end
true
end
end
I thought the stopper was a great solution but eventually I wanted to get rid of it. Unforunately most of our old requests were stilling coming through months later and no one was honoring the 404s. So I decided to monkey patch based off the previous rails versions. This is was I did.
config/initalizers/active_storage.rb
Rails.application.config.after_initialize do
require 'active_storage'
ActiveStorage::Transformers::ImageProcessingTransformer.class_eval do
private
def operations
transformations.each_with_object([]) do |(name, argument), list|
if name.to_s == "combine_options"
list.concat argument.keep_if { |key, value| value.present? and key.to_s != "host" }.to_a
elsif argument.present?
list << [ name, argument ]
end
end
end
end
end
I am using the rack-proxy gem in Rails to proxy requests to an external server. Thing is, the external endpoint requires authentication. How do I provide that information from the middleware?
Here's what I have so far:
require 'rack/proxy'
class MyProxy < Rack::Proxy
MY_REQUEST = %r{^/path/(.*)}
def initialize(app)
#app = app
end
def call(env)
if m = MY_REQUEST.match(env['PATH_INFO'])
env['PATH_INFO'] = "https://otherserver.org/#{m[1]}"
env['HTTP_HOST'] = "otherserver.org"
#the otherserver.org endpoint requires authentication
super env
else
#app.call(env)
end
end
end
Depends on what kind of authentication the other server is using. If its just plain HTTP Authentication you can do something like:
env['Authentication'] = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
Where the value part follows the spec at: http://en.wikipedia.org/wiki/Basic_access_authentication#cite_ref-8
Following the Rack spec here gave me good pointers - http://rubydoc.info/github/rack/rack/master/file/SPEC
Had to do:
env['HTTP_AUTHORIZATION'] = 'Basic <base64 username:password>'
In capybara specs I want to test absence of XSS vulnerability. We use selenium-webdriver with chromium to run browser specs, but chrome by default has XSS protection, which may be disabled by setting X-XSS-Protection header to 0. I wrote a middleware to set this header, and it works if enabled in config/environments/test.rb. As this header is required only in this spec, I don't want to have it enabled for all specs.
I tried following:
describe 'without xss protection' do
before :all do
Rails.configuration.middleware.use Rack::DisableXssProtection
end
after :all do
Rails.configuration.middleware.delete Rack::DisableXssProtection
end
it 'should not have xss', :needs_browser do
visit new_order_path
page.driver.execute_script <<-EOF
$("<input/>", {
id: "new_input",
name: "bad_field",
type: "radio",
value: "<script>alert('fail');</script>"
}).appendTo("#some_form");
EOF
find('#new_input').click
click_on 'submit'
end
end
If I stop anywhere inside this spec, I can see it in Rails.configuration.middleware, but it is not called (header is not set and if I put raise in this middleware it is ignored).
So, how can I add/remove middleware while server is running?
EDIT: middleware is just the following:
module Rack
class DisableXssProtection
def initialize(app)
#app = app
end
def call(env)
status, headers, body = #app.call(env)
headers['X-XSS-Protection'] = '0'
[status, headers, body]
end
end
end
As you're testing Rack::DisableXssProtection itself, it would make sense to extract it as a gem, and test it in isolation with a dummy Rails application.
I am using Rails 2.3 and I decided to provide support for JSONP. Created a brand new application. Then ran script/generate scaffold User name:string
This is my entire environment.rb
RAILS_GEM_VERSION = '2.3.2' unless defined? RAILS_GEM_VERSION
require File.join(File.dirname(__FILE__), 'boot')
require 'rack/contrib'
Rails::Initializer.run do |config|
config.middleware.use 'Rack::JSONP'
end
When I visit localhost:3000/users all I get is a hash. When I visit localhost:3000/users.js?callback=show then I get good result.
Let's look at the jsonp code . I do not understand why response is being wrapped in an array.
I created another Rack middleware where I replaced this statement
[status, headers, [response]]
with this statement
[status, headers, response]
And now everything is working fine.
I refuse to believe that this is a bug in rack-contrib.
Can someone enlighten me why response is being wrapped in an array and how could I use rack-contrib in my application.
The full source code of my application is here. Just clone it and run on localhost:3000 .
That code is wrong. Here's what it should be:
def call(env)
status, headers, response = #app.call(env)
request = Rack::Request.new(env)
if request.params.include?('callback')
response = [pad(request.params.delete('callback'), response)]
headers['Content-Length'] = response.length.to_s
end
[status, headers, response]
end
It was incorrectly wrapping the response in an array in the case where the params didn't include a callback. The reason it needs to wrap the response in an array in the case where params does include a callback is because Rack responses must respond to .each().