Rails Escape Routes using Constraints
Welcome to Escape Routes, here we’ll be using the Rails Routes file to navigate our way through an app. Recently at Createk we’ve been working to remove a technology from a client’s web app as it was making it difficult to modify the app. This required re-routing a lot of the pages and recently brought about an interesting problem, which took some head scratching.
One particular route for a polymorphic model needed to be directed to one of six different Controllers depending on the type of resource passed to it whilst maintaining the same url and path helpers.
To picture this, imagine the age old adage of being faced with six different doors. Behind one is that shiny new Tesla you’ve been pining for, (don’t worry, it’s already constructed, no silly 3 year waiting times). Behind all the others, you fall into the Upside Down, and no-one wants to be there, as everything is upside down (sorry Australia).
What the app had done previously was to wastefully perform two separate requests. One to a determinator Controller which would find the type of the polymorphic model and then make another request via AJAX to the specific controller based on the resource’s type which would then point it to the correct view template.
The pass through get out of jail for (not so) free card
We ran into a similar scenario earlier in this process which had two controllers to choose between. For this, I paired with a colleague who had a neat solution known as a pass through. As there were just two Controllers we allowed the route to first try the controller that it would most likely need. But if it wasn’t the correct one the request would “pass through it” and try the next.
To do this in this initial controller we add a
before_action: like so:
class OneTypeController < ApplicationController
pass_through_to_next_controller method looks like so:
head :ok, ‘X-Cascade’ => ‘pass’
X-Cascadeheader is set in the request env and is a Rack middleware convention that tells the request to pass back through the middleware and look back in the routes file for another route. So we just needed to place the other route underneath and it would fall into the correct one.
The routes look like so:
get ‘resource/:resource_id/’ => ‘one_type#show’, as: :resource_one_type
get ‘resource/:resource_id/’ => ‘another_type#show’, as: :resource_another_type
So this is the equivalent of just picking between two doors, if you get it wrong, well you’ll have to struggle your way through the upside down to find the door at the other end and finally climb into your lovely Tesla (hopefully its charged incase that pesky Demogorgon is hot on your tail). Be careful though, as passing the request all the way back through the middleware stack will slow down your response. Therefore if there are more than two routes for the request to take, it is better to use another option…
Knock Knock Who’s There? — Rails Constraints
So obviously passing through six times would take a bit of time. Luckily, Rails give us something we can work with. It gives us the opportunity to try each door and ask “knock knock, who’s there?” with constraints. Using constraints on a set of routes means that they are only available if the specified requirements are met. You can use Regex for matching and also make them dynamic in which case we can pass a lamda or an argument to the constraint with the request to see if it matches our constraint.
In the routes file we aimed to be as light on the database as possible. So we used:
get ‘resource/:resource_id/’ => ‘one_type#show’, as: resource_one_type
get ‘resource/:resource_id/’ => ‘another_type#show’, as: resource_another_type
and created a constraint in
app/constraints to do the matching:
@type = type
resource = current_resource(request)
resource.type == @type
Here we use the request parameters to load the resource and check its type in the important
matches?(request) method. If it is correct, the routes within the block are available, if not it moves on to the next constraint. It isn’t perfect as we are loading up the resource each time, but it is better than going through the middleware stack each time to find the correct match.
So we get to go up to each door and ask if it is the one we are looking for, if it is we head in, if not we try the next. Oh good, no monsters and we get to stay the right way up, so lets hop in that Tesla and try out that ludicrous mode!
Lastly, we decided to put a fallback in, just incase all the doors happened to be locked. As a truly respectful Escape Routes company we need to handle this gracefully and don’t leave users stuck in limbo. So after our constraint blocks we add:
get 'resource/resource_id' => 'application#render_404', as: :resource_index
This points to a
render_404 method incase nothing matches the constraint and also gives the added bonus of allowing for a generic path helper for these routes.
Constraints proved to be a handy solution to a bit of routing problem. It isn’t perfect as it requires the loading of the resource each time but it saves us having to go all the way through the middleware to then come all the way out again without changing the path helpers or the urls.