Don’t Solve The Problem, Prevent It.
When you deal with input in programming, it is always a good practice to make sure you can’t get from the user what you wouldn’t want to get. I know it sounds strange at the beginning, but let me further explain with the simplest example:
Let’s say you want to get a date as an input from the user. A date can have many different formats and dealing with every possible format could become very frustrating. Now if you let the user enter the date in a simple text box you leave the power in the hands of the user. He decides how the input gets to your server, and you find yourself wasting a lot of code lines on the parsing of the date.
That is exactly the reason why graphical date picker components were created. A date picker lets the user choose from a graphical calendar and injects the formatted selection into the text box, which, obviously, should not be editable. So instead of allowing a broad number of date formats and dealing with the input in our code, we narrowed the input format to exactly what we want to get and we can deal now with business logic instead of date parsing. In other words, we did not solve the problem, we prevented it from ever happening. That’s a great attitude which will spare you a lot of code lines.
ActionDispatch and Problem Prevention
In this context, I wanted to talk about Rails routing a little bit. When we think about it, the ActionDispatch mechanism which resolves routes to controller actions is your (as a programmer) first encounter with user input. We already agreed it is best to prevent a problem, than resolving it in our code, so the routing mechanism is definitely the best place to start looking for code smells.
Let’s take a simple example in which we have a Post
model with title
, content
, author_name
and post_number
attributes. We also want to let the user filter posts by each of these attributes at /posts/filter/content/<some_content_filter>
. One might think of following solution :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# routes.rb | |
match 'posts/filter/:by_attribute/:value' => 'posts#filter' | |
# posts_controller.rb | |
class PostsController < ApplicationController | |
def filter | |
if ["title", "content", "author_name", "post_number"].include? params[:by_attribute] | |
@posts = Post.where(params[:by_attribute] => params[:value]) | |
else | |
render :nothing => true, :status => :not_found | |
end | |
end | |
end |
Although this one will do the job, what we are doing here is solving the problem instead of preventing it. There is no reason, what so ever, to go through a full stack response and not halt the processing of the request the earliest possible – our routing definitions:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# routes.rb | |
match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => { :by_attribute => /^title$|^content$|^author_name$|^post_number$/ } | |
# posts_controller.rb | |
class PostsController < ApplicationController | |
def filter | |
@posts = Post.where(params[:by_attribute] => params[:value]) | |
end | |
end |
This will respond with 404 not found to any filter which does not match the regular expression we defined. A request with an illegal filter won’t even reach our controller code – ActionDispatch deals with it a lot earlier. We benefit both in performance and in less code clutter in our controller.
Correctly Routing Only AJAX Requests
This was a basic example of using the routing mechanism in order to filter unwanted requests.
A more complicated one would be to make some controller actions respond only to AJAX requests. One way to do it would be the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# routes.rb | |
match 'posts/filter/:by_attribute/:value' => 'posts#filter' | |
# posts_controller.rb | |
class PostsController < ApplicationController | |
respond_to :js | |
def filter | |
@posts = Post.where(params[:by_attribute] => params[:value]) | |
respond_with @posts | |
end | |
end |
respond_to :js
and respond_with @posts
make sure our controller will only reply to AJAX requests. I think you know where I’m going now – why not stop the request processing with a routing definition?
An ideal solution could be:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => { :only_ajax => true } |
Now there are some good news and some bad news regarding this code snippet:
The bad news: The :constraints hash does not have a :only_ajax => true
option.
The good news: It is very easy to implement one yourself:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# routes.rb | |
class OnlyAjaxRequest | |
def matches?(request) | |
request.xhr? | |
end | |
end | |
match 'posts/filter/:by_attribute/:value' => 'posts#filter', :constraints => OnlyAjaxRequest.new |
All that is needed is an object which responds to the matches?
method. The Request object is passed as a parameter to it so you can query it for whatever you’d like, including it being an xhr request using the xhr?
method.
This way non-AJAX requests won’t even reach your controller, and you don’t have to deal with scary
respond_to do |format|
blocks.
How about
match ‘posts/filter/:by_attribute/:value’ => ‘posts#filter’,
:constraints => -> (req) { req.xhr? }
Don’t like to see non-route code into the routes file. :p
And you are instantiating a new OnlyAjaxRequest class every time you declare a new full_ajax route, isn’t it ? Why not using Rails route helpers ?
Thanks for the article, nice approach.
:+1
Thanks for the comment I have a few thoughts on it:
1. I was actually not aware of the -> (req) { req.xhr? } syntax to filter only ajax requests. It is really cool thanks for mentioning this up.
2. Regarding mixing non-route code: The right thing to do is to put this code under lib/routes or something like that and have all routes contraints in there.
3. Also, if you find yourself repeating on that -> (req) { req.xhr? } I believe my approach is much more dry.
4. I really think that my approach is more object oriented, readable and reusable than just putting this lambda syntax on the routes.rb file.
Lovely approach, thanks 🙂
Great! very neat solution. Thanks.
Reblogged this on Sutoprise Avenue, A SutoCom Source.
Reblogged this on SyGen development blog and commented:
Burn your controller a la Erez Rabih
Gary Bernhardt agrees: https://www.destroyallsoftware.com/blog/2011/burn-your-controllers