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...
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...
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...
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...
Be very careful with 301 and 308 redirects
Browsers support different types of redirects.
Be very careful with these status codes:
301 Moved Permanently
308 Permanent Redirect
Most browsers seem to cache these redirects forever, unless you set different Cache-Control
headers. If you don't have any cache control headers, you can never change them without forcing users to empty their cache.
Note
By default Rails sends a ...
How to disable logging for ActiveStorage's Disk Service routes
In development, we store files using ActiveStorage's disk
service. This means that stored files are served by your Rails application, and every request to a file results in (at least!) one non-trivial log entry which can be annoying. Here is how to disable those log entries.
Example
Here is an example of what loading a single <img>
in an example application writes to the Rails log.
Started GET "/rails/active_storage/blobs/redirect/..." for ::1 at ...
Processing by ActiveStorage::Blobs::RedirectController#show as SVG
Parameter...
Popular mistakes when using nested forms
Here are some popular mistakes when using nested forms:
- You are using
fields_for
instead ofform.fields_for
. - You forgot to use
accepts_nested_attributes
in the containing model. Rails won't complain, but nothing will work. In particular,nested_form.object
will benil
. - The
:reject_if
option lambda in youraccepts_nested_attributes
call is defined incorrectly. Raise the attributes hash given to your:reject_if
lambda to see if it looks like you expect. - If you are nesting forms into nested forms, each model involved ne...
Compare library versions as "Gem::Version" instances, not as strings
Sometimes we have to write code that behaves differently based on the version of a specific gem or the Ruby Version itself. The version comparison can often be seen with simple string comparison like so.
# ❌ Not recommended
if Rails.version > '6.1.7.8' || RUBY_VERSION > '3.1.4'
raise Error, 'please check if the monkey patch below is still needed'
end
If you are lucky, the version comparison above works by coincidence. But chances are that you are not: For example, Rails version 6.1.10.8
would not raise an error in the code ...
Testing ActiveRecord validations with RSpec
Validations should be covered by a model's spec.
This card shows how to test an individual validation. This is preferrable to save an entire record and see whether it is invalid.
Recipe for testing any validation
In general any validation test for an attribute :attribute_being_tested
looks like this:
- Make a model instance (named
record
below) - Run validations by saying
record.validate
- Check if
record.errors[:attribute_being_tested]
contains the expected validation error - Put the attribute into a valid state
- Run...
ActiveRecord: String and text fields should always validate their length
If you have a :string
or :text
field, you should pair it with a model validation that restricts its length.
There are two motivations for this:
- In modern Rails, database types
:string
and:text
no longer have a relevant size limit. Without a validation a malicious user can quickly exhaust the hard drive of your database server. - In legacy Rails (or database schemas migrated from legacy Rails), database types
:string
and:text
had a database-side length constraint. When the user enters a longer string, the ActiveRecord valida...
Migrating from Elasticsearch to Opensearch: searchkick instructions (without downtime!)
General
A general overview about why and how we migrate can be found under Migrating from Elasticsearch to Opensearch
This card deals with specifics concerning the use of searchkick.
Step 1: Make Opensearch available for Searchkick
In your Gemfile
# Search
gem 'searchkick' # needs to be > 5, to use Opensearch 2
gem 'elasticsearch'
gem 'opensearch-ruby'
in config/initializers/searchkick.rb
(or wherever you have configured your Searchkick settings) add:
SEARCHKICK_CLIENT_T...
CarrierWave: Default Configuration and Suggested Changes
CarrierWave comes with a set of default configuration options which make sense in most cases. However, you should review these defaults and adjust for your project wherever necessary.
You will also find suggestions on what to change below.
Understanding the default configuration
Here is the current default config for version 2:
config.permissions = 0644
config.directory_permissions = 0755
config.storage_engines = {
:f...
Different ways to set attributes in ActiveRecord
Rails 5 / 6 / 7
Method | Uses Default Accessor | Saves to Database | Runs Validations | Runs Callbacks | Updates updated_at/updated_on | Respects Readonly |
---|---|---|---|---|---|---|
attribute= |
Yes | No | n/a | n/a | n/a | n/a |
attributes= |
Yes | No ... |
What we know about PDFKit
What PDFKit is
- PDFKit converts a web page to a PDF document. It uses a Webkit engine under the hood.
- For you as a web developer this means you can keep using the technology you are familar with and don't need to learn LaTeX. All you need is a pretty print-stylesheet.
How to use it from your Rails application
- You can have PDFKit render a website by simply calling
PDFKit.new('http://google.com').to_file('google.pdf')
. You can then send the...
Beware of params with non-string values (nil, array, hash)
Recent rails security updates have shown that people make incorrect assumptions about the possible contents of the params
hash.
Just don't make any! Treat it as what it is: potentially unsafe user input. For example:
/pages/edit?foo= --> params == {:foo => ""}
/pages/edit?foo --> params == {:foo => nil}
/pages/edit?foo[] --> params == {:foo => [nil]} # at least in older rails 3 and in rails 2.x
Be especially wary about stuff like
User.find_by_password_reset_token(params[:password_reset_token])
I...
`simple_format` does not escape HTML tags
simple_format
ignores Rails' XSS protection. Even when called with an unsafe string, HTML characters will not be escaped or stripped!
Instead simple_format
calls sanitize
on each of the generated paragraphs.
ActionView::Base.sanitized_allowed_tags
# => #<Set: {"small", "dfn", "sup", "sub", "pre", "blockquote", "ins", "ul", "var", "samp", "del", "h6", "h5", "h4", "h3", "h2", "h1", "span", "br", "hr", "em", "address", "img", "kbd", "tt", "a", "acrony...
Fixing flaky E2E tests
An end-to-end test (E2E test) is a script that remote-controls a web browser with tools like Selenium WebDriver. This card shows basic techniques for fixing a flaky E2E test suite that sometimes passes and sometimes fails.
Although many examples in this card use Ruby, Cucumber and Selenium, the techniques are applicable to all languages and testing tools.
Why tests are flaky
Your tests probably look like this:
When I click on A
And I click on B
And I click on C
Then I should see effects of C
A test like this works fine...
The many gotchas of Ruby class variables
TLDR: Ruby class variables (@@foo
) are dangerous in many ways. You should avoid them at all cost. See bottom of this card for alternatives.
Class variables are shared between a class hierarchy
When you declare a class variable, it is shared between this and all descending (inheriting) classes. This is rarely what you want.
Class variables are bound at compile-time
Like unqualified constants, class variables are bound to your current scope *whe...
RSpec: Applying stubs only within a block
When you mocked method calls in RSpec, they are mocked until the end of a spec, or until you explicitly release them.
You can use RSpec::Mocks.with_temporary_scope
to have all mocks applied inside a block to be released when the block ends.
Example:
RSpec::Mocks.with_temporary_scope do
allow(Rails).to receive(:env).and_return('production'.inquiry)
puts Rails.env # prints "production"
end
puts Rails.env # prints "test"
Note that, when overriding pre-existing mocks inside the block, they are not reverted to the previously ...
Careful: `fresh_when last_modified: ...` without an object does not generate an E-Tag
To allow HTTP 304 responses, Rails offers the fresh_when
method for controllers.
The most common way is to pass an ActiveRecord instance or scope, and fresh_when
will set fitting E-Tag
and Last-Modified
headers for you. For scopes, an extra query is sent to the database.
fresh_when @users
If you do not want that magic to happen, e.g. because your scope is expens...
Preloaded associations are filtered by conditions on the same table
When you eagerly load an association list using the .include
option, and at the same time have a .where
on an included table, two things happen:
- Rails tries to load all involved records in a huge single query spanning multiple database tables.
- The preloaded association list is filtered by the
where
condition, even though you only wanted to use thewhere
condition to filter the containing model.
The second case's behavior is mostly unexpected, because pre-loaded associations usually don't care about the circumstances under whi...
Project management best practices: Technical debt summary
Maintaining larger projects makes it more difficult to balance refactoring and upgrade tasks according to its actual value. Consider to create and periodically maintain a summary, which helps you and your team in the decision which refactoring task should be taken next.
Template
Here is an template on how you might categorize your tasks:
| Technical debt | Estimated Efforts | Visible customer value| Customer value explained| Developer value|Developer value explained|
|-----------------------------|----------------|----------...
Eager-loading polymorphic associations
To avoid n+1 queries, you want to eager-load associated records if you know you need to access them later on.
The Rails docs say:
Eager loading is supported with polymorphic associations.
This is true, but has some caveats.
Example
Consider the following models:
class Image < ActiveRecord::Base; end
class Video < ActiveRecord::Base; end
class PageVersion < ActiveRecord::Base
belongs_to :primary_medium, polymorphic: true # may be Image or Video
end
class Page < ActiveRecord::Base
belongs_to ...
Writing a README for a project
Rails applications and ruby gems should have a README that gives the reader a quick overview of the project. Its size will vary as projects differ in complexity, but there should always be some introductory prose for a developer to read when starting on it.
Purpose
That's already the main purpose of a project README: Give a new developer a quick overview of the project. In sketching this outline, the README should notify the reader of any peculiarity he needs to know of.
Remember that in a few months, you'll be a kind of "new ...