Allowing dots in Rails routes
Tags: rails
Published: 2020-07-09 04:50:42

---
title: Allowing dots in Rails routes
published: true
date: 2020-06-15 04:53:49 UTC
tags: rails
canonical_url: https://prathamesh.tech/2020/06/15/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.

<!--kg-card-begin: code-->
```
post '/users/:id/upgrade', to: 'upgrade#create'
```
<!--kg-card-end: code-->

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.

<!--kg-card-begin: code-->
```
$ 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
```
<!--kg-card-end: code-->

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.

<!--kg-card-begin: code-->
```
re = /\/users\/([^\/.?]+)\/upgrade/
```
<!--kg-card-end: code-->

> This is simplified form of the actual regular expression used by Rails. Actual regular expression is `/\A/users/([^/.?]+)/upgrade(?:.([^/.?]+))?\Z/`

<!--kg-card-begin: code-->
```
> "/users/john.smith/upgrade".match? re
=> false
```
<!--kg-card-end: code-->

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`.

<!--kg-card-begin: code-->
```
post '/users/:id/upgrade', to: 'upgrade#create', constraints: { id: /[^\/]+/ }
```
<!--kg-card-end: code-->

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](https://guides.rubyonrails.org/routing.html#http-verb-constraints).

We can also add the constraint more succinctly as follows.

<!--kg-card-begin: code-->
```
post '/users/:id/upgrade', to: 'upgrade#create', id: /[^\/]+/
```
<!--kg-card-end: code-->

Let's see what regular expression Rails uses in this case.

<!--kg-card-begin: code-->
```
re = /\/users\/([^\/]+)\/upgrade/

> "/users/john.smith/upgrade".match? re
=> true
```
<!--kg-card-end: code-->

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`.

<!--kg-card-begin: hr-->
* * *
<!--kg-card-end: hr-->

Subscribe to [my newsletter](https://prathamesh.tech/mailing-list) to know more such insights about Ruby on Rails.