ActiveStorage: Old urls still request deprecated :combine_options in variation_key - ruby-on-rails

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

Related

How to make Rack::Attack work behind a load balancer?

I used the example throttle code for Rack::Attack.
throttle('req/ip', limit: 100, period: 5.minutes) do |req|
req.ip unless req.path.starts_with?('/assets')
end
This worked great on our staging server but immediately ran into the limit on production because req.ip returns the IP address of our load balancer and not the remote_ip of the client.
Note that remote_ip is a method in ActionDispatch::Request but not Rack::Attack::Request.
We are using Rails 3.2.2 on Ruby 2.2.
I was able to get it working by adding a method to Rack::Attack::Request
class Rack::Attack
class Request < ::Rack::Request
def remote_ip
#remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s
end
end
end
then using
req.remote_ip unless req.path.starts_with?('/assets')

Uninitialized constant error after uploading on Heroku

There is the following problem: I'm developing some Rails application on my local machine, and all is good, app works, but after uploading on Heroku there would be the following error (I saw it using 'heroku logs'):
NameError (uninitialized constant Api::V1::ApiV1Controller::UndefinedTokenTypeError)
My code:
def require_token
begin
Some code which generates UndefinedTokenTypeError
rescue UndefinedTokenTypeError => e
render json: e.to_json
end
end
UndefinedTokenTypeError is in lib/errors.rb file:
class EmptyCookieParamsError < StandardError
def to_json
{ result_code: 1 }
end
end
class UndefinedTokenTypeError < StandardError
def to_json
{ result_code: 2 }
end
end
I've got the same version for Rails/Ruby on my local machine (2.0). How can I fix it? Thanks.
From what I can see, you may be experiencing either a CORS-related issue or you're not authenticating properly
Cross Origin Resource Sharing
CORS is a standard HTML protocol, which basically governs which websites can "ping" your site. Facebook & Twitter's third-party widgets only work because they allow any site to send them data
For Rails to work with CORS, it's recommended to install the Rack-CORS gem. This will allow you to put this code in your config/application.rb file:
#CORS
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '/data*', :headers => :any, :methods => :post
end
end
Because you're experiencing these issues on Heroku, it could be the problem you're experiencing. Even if it isn't, it's definitely useful to appreciate how CORS works
Authentication
Unless your API is public, you'll likely be authenticating the requests
The way we do this is with the authenticate_or_request_with_http_token function, which can be seen here:
#Check Token
def restrict_access
authenticate_or_request_with_http_token do |token, options|
user = User.exists?(public_key: token)
#token = token if user
end
end
We learnt how to do this with this Railscast, which discusses how to protect an API. The reason I asked about your code was because the above works for us on Heroku, and you could gain something from it!
Running on Heroku will be using the production environment. Check to see what is different between environments/development.rb and environments/production.rb
You can try running your app in production mode on your local machine, rails server -e production
I am guessing your config.autoload_paths isn't set correctly. Should be in config/application.rb

Rails 3.2.x - log elapsed request time

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?

undefined class/module X in Rails 3 despite requiring models in initializer

We use Rails.cache and see an undefined class/module X error when loading pages that invoke class X. We followed the suggestion here to include an initializer that requires all models in the dev environment.
However, we see the same error. Any other suggestions? We included the initializer code below, and from the output to the console, it appears like the code is getting invoked.
We're on Rails 3.2.12 + Ruby 1.9.3.
if Rails.env == "development"
Dir.foreach("#{Rails.root}/app/models") do |model_name|
puts "REQUIRING DEPENDENCY: #{model_name}"
require_dependency model_name unless model_name == "." || model_name == ".."
end
end
Stack trace:
Processing by DandyController#get_apps as JSON
Parameters: {"category"=>"featured", "country_id"=>"143441", "language_id"=>"EN"}
WARNING: Can't verify CSRF token authenticity
Completed 500 Internal Server Error in 2ms
ArgumentError (undefined class/module App):
app/controllers/dandy_controller.rb:66:in `get_featured_apps'
app/controllers/dandy_controller.rb:50:in `get_apps'
Rendered C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/actionpack-3.2.12/lib/action_dispatch/middleware/templat
es/rescues/_trace.erb (2.0ms)
Rendered C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/actionpack-3.2.12/lib/action_dispatch/middleware/templat
es/rescues/_request_and_response.erb (2.0ms)
Rendered C:/RailsInstaller/Ruby1.9.3/lib/ruby/gems/1.9.1/gems/actionpack-3.2.12/lib/action_dispatch/middleware/templat
es/rescues/diagnostics.erb within rescues/layout (46.0ms)
Code:
def get_featured_apps( country_id )
# Fetch featured apps from cache or from DB
apps = Rails.cache.fetch( 'featured_apps', {:expires_in => 1.days} ) do
logger.info '+++ Cache miss: featured apps'
get_featured_apps_helper country_id
end
# Get sponsored results
apps = get_featured_apps_helper country_id
# Return featured apps
return apps
end
I've overwritten the fetch method which worked wonders for me!
unless Rails.env.production?
module ActiveSupport
module Cache
class Store
def fetch(name, options = nil)
if block_given?
yield
end
end
end
end
end
end
A more elegant solution is to write a wrapper method you can call around your call to Rails.cache.fetch:
def some_func
apps = with_autoload_of(App) do
Rails.cache.fetch( 'featured_apps', {:expires_in => 1.days} ) do
logger.info '+++ Cache miss: featured apps'
get_featured_apps_helper country_id
end
end
end
##
# A simple wrapper around a block that requires the provided class to be
# auto-loaded prior to execution.
#
# This method must be passed a block, which it always yields to.
#
# This is typically used to wrap `Rails.cache.fetch` calls to ensure that
# the autoloader has loaded any class that's about to be referenced.
#
# #param [Class] clazz
# The class to ensure gets autoloaded.
#
def self.with_autoload_of(clazz)
yield
end

How can I disable logging in Ruby on Rails on a per-action basis?

I have a Rails application that has an action invoked frequently enough to be inconvenient when I am developing, as it results in a lot of extra log output I don't care about. How can I get rails not to log anything (controller, action, parameters, completion time, etc.) for just this one action? I'd like to conditionalize it on RAILS_ENV as well, so logs in production are complete.
Thanks!
You can silence the Rails logger object:
def action
Rails.logger.silence do
# Things within this block will not be logged...
end
end
Use lograge gem.
Gemfile:
gem 'lograge'
config/application.rb:
config.lograge.enabled = true
config.lograge.ignore_actions = ['StatusController#nginx', ...]
The following works with at least Rails 3.1.0:
Make a custom logger that can be silenced:
# selective_logger.rb
class SelectiveLogger < Rails::Rack::Logger
def initialize app, opts = {}
#app = app
#opts = opts
#opts[:silenced] ||= []
end
def call env
if #opts[:silenced].include?(env['PATH_INFO']) || #opts[:silenced].any? {|silencer| silencer.is_a?( Regexp) && silencer.match( env['PATH_INFO']) }
Rails.logger.silence do
#app.call env
end
else
super env
end
end
end
Tell Rails to use it:
# application.rb
config.middleware.swap Rails::Rack::Logger, SelectiveLogger, :silenced => ["/remote/every_minute", %r"^/assets/"]
The example above shows silencing asset serving requests, which in the development environment means less ( and sometimes no) scrolling back is required to see the actual request.
The answer turns out to be a lot harder than I expected, since rails really does provide no hook to do this. Instead, you need to wrap some of the guts of ActionController::Base. In the common base class for my controllers, I do
def silent?(action)
false
end
# this knows more than I'd like about the internals of process, but
# the other options require knowing even more. It would have been
# nice to be able to use logger.silence, but there isn't a good
# method to hook that around, due to the way benchmarking logs.
def log_processing_with_silence_logs
if logger && silent?(action_name) then
#old_logger_level, logger.level = logger.level, Logger::ERROR
end
log_processing_without_silence_logs
end
def process_with_silence_logs(request, response, method = :perform_action, *arguments)
ret = process_without_silence_logs(request, response, method, *arguments)
if logger && silent?(action_name) then
logger.level = #old_logger_level
end
ret
end
alias_method_chain :log_processing, :silence_logs
alias_method_chain :process, :silence_logs
then, in the controller with the method I want to suppress logging on:
def silent?(action)
RAILS_ENV == "development" && ['my_noisy_action'].include?(action)
end
You can add the gem to the Gemfile silencer.
gem 'silencer', '>= 1.0.1'
And in your config/initializers/silencer.rb :
require 'silencer/logger'
Rails.application.configure do
config.middleware.swap Rails::Rack::Logger, Silencer::Logger, silence: ['/api/notifications']
end
The following works with Rails 2.3.14:
Make a custom logger that can be silenced:
#selective_logger.rb
require "active_support"
class SelectiveLogger < ActiveSupport::BufferedLogger
attr_accessor :silent
def initialize path_to_log_file
super path_to_log_file
end
def add severity, message = nil, progname = nil, &block
super unless #silent
end
end
Tell Rails to use it:
#environment.rb
config.logger = SelectiveLogger.new config.log_path
Intercept the log output at the beginning of each action and (re)configure the logger depending on whether the action should be silent or not:
#application_controller.rb
# This method is invoked in order to log the lines that begin "Processing..."
# for each new request.
def log_processing
logger.silent = %w"ping time_zone_table".include? params[:action]
super
end
With Rails 5 it gets more complicated request processing is logged in several classes. Firstly we need to override call_app in Logger class, let's call this file lib/logger.rb:
# original class:
# https://github.com/rails/rails/blob/master/railties/lib/rails/rack/logger.rb
require 'rails/rack/logger'
module Rails
module Rack
class Logger < ActiveSupport::LogSubscriber
def call_app(request, env) # :doc:
unless Rails.configuration.logger_exclude.call(request.filtered_path)
instrumenter = ActiveSupport::Notifications.instrumenter
instrumenter.start "request.action_dispatch", request: request
logger.info { started_request_message(request) }
end
status, headers, body = #app.call(env)
body = ::Rack::BodyProxy.new(body) { finish(request) }
[status, headers, body]
rescue Exception
finish(request)
raise
ensure
ActiveSupport::LogSubscriber.flush_all!
end
end
end
end
Then follow with lib/silent_log_subscriber.rb:
require 'active_support/log_subscriber'
require 'action_view/log_subscriber'
require 'action_controller/log_subscriber'
# original class:
# https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/log_subscriber.rb
class SilentLogSubscriber < ActiveSupport::LogSubscriber
def start_processing(event)
return unless logger.info?
payload = event.payload
return if Rails.configuration.logger_exclude.call(payload[:path])
params = payload[:params].except(*ActionController::LogSubscriber::INTERNAL_PARAMS)
format = payload[:format]
format = format.to_s.upcase if format.is_a?(Symbol)
info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
info " Parameters: #{params.inspect}" unless params.empty?
end
def process_action(event)
return if Rails.configuration.logger_exclude.call(event.payload[:path])
info do
payload = event.payload
additions = ActionController::Base.log_process_action(payload)
status = payload[:status]
if status.nil? && payload[:exception].present?
exception_class_name = payload[:exception].first
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
additions << "Allocations: #{event.allocations}" if event.respond_to? :allocations
message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
message << " (#{additions.join(" | ")})" unless additions.empty?
message << "\n\n" if defined?(Rails.env) && Rails.env.development?
message
end
end
def self.setup
# unsubscribe default processors
ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
case subscriber
when ActionView::LogSubscriber
self.unsubscribe(:action_view, subscriber)
when ActionController::LogSubscriber
self.unsubscribe(:action_controller, subscriber)
end
end
end
def self.unsubscribe(component, subscriber)
events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
events.each do |event|
ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
if listener.instance_variable_get('#delegate') == subscriber
ActiveSupport::Notifications.unsubscribe listener
end
end
end
end
end
# subscribe this class
SilentLogSubscriber.attach_to :action_controller
SilentLogSubscriber.setup
Make sure to load modified modules e.g. in config/application.rb after loading rails:
require_relative '../lib/logger'
require_relative '../lib/silent_log_subscriber'
Finally configure excluded paths:
Rails.application.configure do
config.logger_exclude = ->(path) { path == "/health" }
end
As we're modifying core code of Rails it's always good idea to check original classes in Rails version you're using.
If this looks like too many modifications, you can simply use lograge gem which does pretty much the same with few other modifications. Although the Rack::Loggger code has changed since Rails 3, so you might be loosing some functionality.
#neil-stockbridge 's answer not worked for Rails 6.0, I edit some to make it work
# selective_logger.rb
class SelectiveLogger
def initialize app, opts = {}
#app = app
#opts = opts
#opts[:silenced] ||= []
end
def call env
if #opts[:silenced].include?(env['PATH_INFO']) || #opts[:silenced].any? {|silencer| silencer.is_a?( Regexp) && silencer.match( env['PATH_INFO']) }
Rails.logger.silence do
#app.call env
end
else
#app.call env
end
end
end
Test rails app to use it:
# application.rb
config.middleware.swap Rails::Rack::Logger, SelectiveLogger, :silenced => ["/remote/every_minute", %r"^/assets/"]
Sprockets-rails gem starting from version 3.1.0 introduces implementation of quiet assets. Unfortunately it's not flexible at this moment, but can be extended easy enough.
Create config/initializers/custom_quiet_assets.rb file:
class CustomQuietAssets < ::Sprockets::Rails::QuietAssets
def initialize(app)
super
#assets_regex = %r(\A/{0,2}#{quiet_paths})
end
def quiet_paths
[
::Rails.application.config.assets.prefix, # remove if you don't need to quiet assets
'/ping',
].join('|')
end
end
Add it to middleware in config/application.rb:
# NOTE: that config.assets.quiet must be set to false (its default value).
initializer :quiet_assets do |app|
app.middleware.insert_before ::Rails::Rack::Logger, CustomQuietAssets
end
Tested with Rails 4.2
Rails 6. I had to put this in config/application.rb, inside my app's class definition:
require 'silencer/logger'
initializer 'my_app_name.silence_health_check_request_logging' do |app|
app.config.middleware.swap(
Rails::Rack::Logger,
Silencer::Logger,
app.config.log_tags,
silence: %w[/my_health_check_path /my_other_health_check_path],
)
end
That leaves the log_tags config intact and modifies the middleware before it gets frozen. I would like to put it in config/initializers/ somewhere tucked away but haven't figured out how to do that yet.

Resources