How to use html_safe correctly

By default, Rails views escape HTML in any strings you insert. If you want to insert HTML verbatim, you need to call #html_safe. However, #html_safe does not "unescape" a string. It merely marks a string as safe for unescaped insertion.

How html_safe works

Calling html_safe on a String returns a new object that looks and acts like a String, but actually is a ActiveSupport::SafeBuffer:

"foo".length
# => 3
"foo".class
# => String

"foo".html_safe.length
# => 3
"foo".html_safe.class
# => ActiveSupport::S...

Implementing authentication and authorization for ActiveStorage blobs/files

ActiveStorage does not provide any built-in way of implementing authentication for the available DirectUpload endpoint in Rails. When using DirectUpload as JS wrapper in the frontend, be aware that its Rails endpoint is public by default, effectively allowing anyone to upload an unlimited amount of files to your storage.

The DirectUploadController from @rails/activestorage bypasses your form controller because it uploads the file using an AJAX request that runs directly, before any form roundtrip happens. This is a comfortable solutio...

Dealing with I18n::InvalidPluralizationData errors

When localizing model attributes via I18n you may run into errors like this:

I18n::InvalidPluralizationData: translation data { ... } can not be used with :count => 1. key 'one' is missing.

They seem to appear out of the blue and the error message is more confusing than helpful.

TL;DR A model (e.g. Post) is lacking an attribute (e.g. thread) translation.
Fix it by adding a translation for that model's attribute (attributes.post.thread). The error message reveals the (wrongly) located I18n data (from `attributes.thread...

Nested ActiveRecord transaction pitfalls

When working with custom transactions and use ActiveRecord::Rollback you may encounter unexpected behaviour if you try to roll back your changes.

tl;dr

When using nested transactions, ActiveRecord::Rollback might not do what you expect, since it will only roll back the inner, but not the outer transaction.

You can fix this behavior by using transaction(joinable: false) but this leads to a bunch of different problems.

When you don't need an explicit ActiveRecord::Rollback, don't worry about any of this and just use a plan `tran...

Do not pass params directly into url_for or URL helpers

Rails' url_for is useful for generating routes from a Hash, but can lead to an open redirect vulnerability.

Your application's generated route methods with a _url suffix are also affected because [they use url_for unter the hood](https://github.com/rails/rails...

Delivering Carrierwave attachments to authorized users only

Preparation

To attach files to your records, you will need a new database column representing the filename of the file. To do this, add a new migration (rails g migration <name>) with the following content:

class AddAttachmentToNotes < ActiveRecord::Migration[6.0]
  def change
    add_column :notes, :attachment, :string
  end
end

Don't forget to rename the class and change the column details to fit your purpose. Run it.

1) Deliver attachments through Rails

The first way is to store your Carrierwave attachments not ...

Configuring ActionMailer host and protocol for URL generation

When you generate a URL in a mailer view, ActionMailer will raise an error unless you previously configured it which hostname to use.

There are two options to set the default_url_options of ActionMailer:

  1. Hardcoded solution (preferred solution when using Rails with ActiveJob/Sidekiq or Cronjobs)
  2. Dynamic solution

1. Hardcoded solution

When you are sending mails from outside the request cycle, e.g. ActiveJob/Sidekiq or Cronjobs, y...

Capistrano + Rails: Automatically skipping asset compilation when assets have not changed

In medium-sized to large Rails applications, asset compilation can take several minutes. In order to speed up deployment, asset precompilation can be skipped. This card automates the process.

Capistrano 3

namespace :deploy do
  
  desc 'Automatically skip asset compile if possible'
  task :auto_skip_assets do
    asset_locations = %r(^(Gemfile\.lock|app/assets|lib/assets|vendor/asset))

    revisions = []
    on roles :app do
      within current_path do
        revisions << capture(:cat, 'REVISION').strip
      ...

Using feature flags to stabilize flaky E2E tests

A flaky test is a test that is often green, but sometimes red. It may only fail on some PCs, or only when the entire test suite is run.

There are many causes for flaky tests. This card focuses on a specific class of feature with heavy side effects, mostly on on the UI. Features like the following can amplify your flakiness issues by unexpectedly changing elements, causing excessive requests or other timing issues:

  • Lazy loading images
  • Autocomplete in search f...

SameSite cookies

TL;DR Most web applications do not require action on this. SameSite=None (old browser default) will continue to work, and SameSite=Lax (new Chrome default, gradually rolled out) is an even better default for cookies. Set SameSite=Strict only for extra security in special cases (see below). If your application is rendered in an iframe (e.g. a video player or some news stream), you need to configure its relevant cookies as SameSite=None.


The SameSite cookie attribute targets **c...

better rails app restart with the passenger restart-app tool

With this command you can initiate an application restart without touching restart.txt. Unlike touching restart.txt, this tool initiates the restart immediately instead of on the next request. http://blog.phusion.nl/2014/01/02/phusion-passenger-4-0-33-released/

If you want to use this with capistrano 2.x just replace the touch command:

-    run "touch #{current_path}/tmp/restart.txt"
+    run "passenger-config restart-app --ignore-app-not-running #{deploy_to}...

Best practice: How to manage versions in a Gemfile

It most cases it's not necessary to add a version constraint next to your gems in the Gemfile. Since all versions are saved in the Gemfile.lock, everyone running bundle install will get exactly the same versions.

There are some exceptions, where you can consider adding a version constrain to the Gemfile:

  • You are not checking in the Gemfile.lock into the version control (not recommended)
  • A specific gem has a bug in a more recent version (adding a comment for the reason is highly recommended)
  • You want to ensure no one upgrade...

Rails: Configure ActionController to raise when strong params are invalid

Put the line below in the respective env.rb file to make your action controllers raise an ActionController::UnpermittedParameters error when strong params are not valid. This might come in handy when you are implementing an API and you want to let the user know that the request structure was invalid. You can then rescue those errors by implementing a rescue_from to have a generic handling of those cases. Note that you might need to whitelist common params such as :format to not raise on valid requests.

config.action_controller.a...

Load order of the environment

Rails 3, 4, 5, 6

  1. config/application.rb
  2. config/environment.rb before the initialize! call (we don't usually edit this file)
  3. The current environment, e.g. environments/production.rb
  4. Gems
  5. Vendored plugins
  6. All initializers in config/initializers/*.rb
  7. config/environment.rb after the initialize! call (we don't usually edit this file)
  8. Your own code from app

Rails 2

  1. Code in config/preinitializer.rb (if it exists)
  2. environment.rb, code above the Rails::Initializer.run blo...

Timeouts for long-running SQL queries

While the main goal always is to prevent long-running queries in the first place, automatic timeouts can serve as a safety net to terminate problematic queries automatically if a set time limit is exceeded. This prevents single queries from taking up all of your database’s resources and reduces the need for manual intervention that might destabilize or even crash the application.

As Rails does not set a timeout on database statements by default, the following query will run for an entire day:

ActiveRecord::Base.connection.execute("S...

Webpack(er): A primer

webpack is a very powerful asset bundler written in node.js to bundle (ES6) JavaScript modules, stylesheets, images, and other assets for consumption in browsers.

Webpacker is a wrapper around webpack that handles integration with Rails.

This is a short introduction.

Installation

If you haven't already, you need to install node.js and Yarn.

Then, put

gem 'webpacker', '~> 4.x' # check if 4.x is still cu...

Action Mailer Previews

Rails includes a way to see what an e-mail will look like.

Integration to RSpec

All you need to do is implement a preview-class in spec/mailers/previews/notifier_preview.rb:

class NotifierPreview < ActionMailer::Preview
  def welcome
    Notifier.welcome(User.first)
  end
end

And adapt the preview load path in your application.rb:

config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews" # For Rails < 7.1
config.action_mailer.preview_paths << "#{Rails.root}/spec/mailers/previews" # For Rails >=...

Using rack-mini-profiler (with Unpoly)

Debugging performance issues in your Rails app can be a tough challenge.

To get more detailed insights consider using the rack-mini-profiler gem.

Setup with Unpoly

Add the following gems:

group :development do
  gem 'memory_profiler'
  gem 'rack-mini-profiler'
  gem 'stackprof'
end

Unpoly will interfere with the rack-mini-profiler widget, but configuring the following works okayish:

// rack-mini-profiler + unpoly
if (process...

ActiveRecord: Passing an empty array into NOT IN will return no records

Caution when using .where to exclude records from a scope like this:

# Fragile - avoid
User.where("id NOT IN (?)", excluded_ids)

When the exclusion list is empty, you would expect this to return all records. However, this is not what happens:

# Broken example
User.where("id NOT IN (?)", []).to_sql
=>  SELECT `users`.* FROM `users` WHERE (id NOT IN (NULL))

Passing an empty exclusion list returns no records at all! See below for better implementations.

Rails 4+

Use the .not method to let Rails do the logic

`...

PostgreSQL: Difference between text and varchar columns

TL;DR PostgreSQL handles Rails 4+ text and string columns the same. Some libraries may still reflect on the column type and e.g. render differently sized text fields.


PostgreSQL offers three character types for your columns:

  • character varying(n) (also called varchar or just string): Contents are limited to n characters, smaller contents are allowed.
  • character(n): All contents are padded with spaces to allocate exactly n characters.
  • text: There is no upper or lower character limit (except for the absolute...

HTTP 302 redirects for PATCH or DELETE will not redirect with GET

A HTTP 302 Found redirect to PATCH and DELETE requests will be followed with PATCH or DELETE. Redirect responses to GET and POST will be followed with a GET. The Rails form_for helper will use a workaround to send POST requests with a _method param to avoid this issue for PATCH/DELETE.

If you make requests yourself, watch out for the following behavior.

When you make an AJAX request PATCH /foo and the /foo action redirects to /bar, browsers will request PATCH /bar. You probably expected the second requ...

Preventing users from uploading malicious content

When you allow file uploads in your app, a user might upload content that hurts other users.

Our primary concern here is users uploading .html or .svg files that can run JavaScript and possibly hijack another user's session.

A secondary concern is that malicious users can upload executables (like an .exe or .scr file) and use your server to distribute it. However, modern operating systems usually warn before executing files that were downloaded from t...

High-level data types with "composed_of"

I recently stumbled upon the Rails feature composed_of. One of our applications dealt with a lot of addresses and they were implemented as 7 separate columns in the DB and Rails models. This seemed like a perfect use case to try out this feature.

TLDR

The feature is still a VERY leaky abstraction. I ran into a lot of ugly edge cases.

It also doesn't solve the question of UI. We like to use simple_form. It's currently not possible to simply write `f...

How to update a single gem conservatively

The problem

Calling bundle update GEMNAME will update a lot more gems than you think. E.g. when you do this:

bundle update cucumber-rails

... you might think this will only update cucumber-rails. But it actually updates cucumber-rails and all of its dependencies. This will explode in your face when one of these dependencies release a new version with breaking API changes. Which is all the time.

In the example above updating cucumber-rails will give you Capybara 2.0 (because capybara is a dependency of `cucumber-rail...