DEV Community

Morten Trolle
Morten Trolle

Posted on

Don't use instance variables in Rails Validators

Take a look at this code:

class Comment < ApplicationRecord validates_with Validators::CommentContentValidator end module Validators class CommentContentValidator def validate(comment) @comment = comment if contains_bad_language? @comment.errors.add(:comment, :bad_language) end if contains_unwanted_urls? @comment.errors.add(:comment, :bad_url) end end def contains_bad_language? # Imagine this takes a few seconds BadWords.find_each do |bad_word| break true if bad_word.has_a_bard_word?(@comment.message) end end def contains_bad_urls? @comment.message.match?(/.../) end end end 
Enter fullscreen mode Exit fullscreen mode

A first glance it looks harmless right? And you might not see any issues for a long time.

But Rails actually initializes your Validators::CommentContentValidator at the time it loads the Comment model and the result is your instance variable @comment is not thread safe in your controllers.
If you get many requests saving comments at the same time you'll end up mixing up the @comment between requests.

E.g. imagine these two requests happens in the same split second:

request_a: Comment.create!(...) # contains a bad url request_b: Comment.create!(...) # is valid request_a: Calls Validators::CommentContentValidator#validate(comment) request_b: Calls Validators::CommentContentValidator#validate(comment) 
Enter fullscreen mode Exit fullscreen mode

Request A starts out validating bad words, but before it's done request B's validation has started and now @comment in Validators::CommentContentValidator is overwritten by request B's Comment.

When request A gets to contains_bad_urls? it is now using the comment of request B and thereby making request A valid.

The solution
If you are using custom validators, then make sure to pass the variable along in all method calls:

module Validators class CommentContentValidator def validate(comment) if contains_bad_language?(comment) comment.errors.add(:comment, :bad_language) end if contains_unwanted_urls?(comment) comment.errors.add(:comment, :bad_url) end end def contains_bad_language?(comment) # Imagine this takes a few seconds BadWords.find_each do |bad_word| break true if bad_word.has_a_bard_word?(comment.message) end end def contains_bad_urls?(comment) comment.message.match?(/.../) end end end 
Enter fullscreen mode Exit fullscreen mode

Top comments (0)