Allowing dots in Rails routes
Rails routing is very powerful and it allows to define different types of routes. Though resourceful routes is recommended, it also gives us ability to define routes for specific cases using dynamic segments.
post '/users/:id/upgrade', to: 'upgrade#create'
This route matches users/123/upgrade
and provides params[:id]
with value 123
.
All good so far. Now, we want to rather support users/john.smith/upgrade
to make the URLs user friendly. As we are using the dynamic id
segment in the route already, hopefully we will get that value in the params[:id]
.
Let's try hitting our application with this URL.
$ curl -iv "http://localhost:3000/users/john.smith/upgrade"
< HTTP/1.1 404 Not Found
< Content-Type: text/html; charset=UTF-8
< X-Web-Console-Session-Id: 8e09e5f6d9e8308f0647622457060bde
< X-Web-Console-Mount-Point: /__web_console
< X-Request-Id: bac4bb34-0577-4134-abba-2f8d6aa437f0
< X-Runtime: 0.101339
< Transfer-Encoding: chunked
Surprisingly, instead of getting expected response, we get a 404 response!
How Rails parses URLs
One of the lesser known thing about how Rails matches the route segments with incoming requests.
The default regular expression used for a route segment is /[^\/.?]+/
. It matches any combination of characters that is not a forward slash, dot or question mark.
Because of this, when we hit users/john.smith/upgrade
, it is matched with following regular expression by Rails.
re = /\/users\/([^\/.?]+)\/upgrade/
This is simplified form of the actual regular expression used by Rails. Actual regular expression is /\A/users/([^/.?]+)/upgrade(?:.([^/.?]+))?\Z/
> "/users/john.smith/upgrade".match? re
=> false
As we can see, the regular expression does not match with the request if the id
segment has a dot
inside it.
Constraints to the rescue
Rails supports adding URL constraints for every route which can be used to mitigate this problem. We can add HTTP verb based constraints, request attributes based constraints as well as dynamic segment based constraints. In this case, we will add constraint on the dynamic segment id
.
post '/users/:id/upgrade', to: 'upgrade#create', constraints: { id: /[^\/]+/ }
This defines a constraint on :id
such that it should be any combination of characters other than forward slash
. Now our requests containing dot
character will get resolved by the Rails router.
More information about how to add constraints can be found in Rails guides.
We can also add the constraint more succinctly as follows.
post '/users/:id/upgrade', to: 'upgrade#create', id: /[^\/]+/
Let's see what regular expression Rails uses in this case.
re = /\/users\/([^\/]+)\/upgrade/
> "/users/john.smith/upgrade".match? re
=> true
As per our instructions, the :id
segment is now getting matched with /[^\/]+/
instead of the default regular expression used by Rails.
Question about question mark
Although Rails uses
as regular expression for route segments, by the time the request hits the Rails router, the /[^\/.?]+/
?
is already separated into query string and is no longer part of the path which is matched with the routes defined in our application.
For eg. consider we are hitting the application with users/john?smith/upgrade
Rails will try to find a route with users/john
in this case which will result into 404 if there is no such route.
Takeaway
Rails uses dot
and forward slash
as separators while parsing the route segments. They can only be matched when explicitly requested using the constraints
.
Subscribe to my newsletter to know more such insights about Ruby on Rails.