In my last two posts about Rack, I wrote about the basics of Rack and how to write middleware. If you have no idea what this is about, I recommend reading the last two posts (in the order above). For the rest of you, carry on - today we will see how to write awesome Rails middleware and how to use it in any Rails application. Rails and Rack play together really nice, so keep on reading!
Rails on Rack
Rails by default has bunch of middleware loaded that is crucial to Rails' working. If you want to see what middleware your Rails app is using, open it up in command line and run:
You will see a big list of middleware classes that your current Rails app is using. This is a sample output on a app that I am working on. Keep in mind that your output may vary.
The list of middleware is quite big. If you are curious what any of these middleware classes do, check this list out. Cool stuff, right? Lets see what rules and conventions apply to writing Rails middleware and how we can leverage those to write our own middleware.
Setting up a middleware class
Just like any Rack middleware that we wrote before, we will need to write a
middleware class. Since v3.2, Rails gave us the ability to add middleware
lib/middleware. So, lets add that class.
As always, there are some conventions of how Rails middleware should be
created. Like any other middleware class, the class needs an
method and a
call method. Also, the first argument of the initialize
method is the application itself, and the first argument of the call method is
the request environment.
Using our middleware
Rails allows us to mount this middleware class in the middleware stack so the application can use it in runtime. Again, you can see the middleware stack by running
in your command line.
Mounting middleware is usually done in the
config/application.rb file. But,
Rails also allows us to mount different middleware for different environment.
This means that you can mount middleware in any of the
files. Mounting a middleware class is done with the command:
Or, in our case:
There is this tiny caveat when mounting middleware - note that the class name
is written as a string, not as a constant. If you mount a middleware class in
config/application.rb file the class name has to be a string. But, if
you load the middleware class in the environment files (i.e.
config/environment/development.rb) you can mount is as a constant:
As far as I can tell, the reason for this tiny caveat is when Rails is loading
all the required files on server boot, when
config/application.rb is loaded
not all constants are loaded. But, due to the nature of the envronment files,
they seem to be autoloaded at the end, when all of the constants are present in
Mounting the middleware in the stack
Adding our middleware at a certain point of the middleware stack is done via
For example, if we, for whatever reason, want to add our
just before the
Rails::Rack::Logger middleware, we can use this line:
On the flipside, if we want to add our
MyMiddleware class after the
Rails::Rack::Logger middleware, we can use this line:
Another convenience method that Rails provides us is
swap, as in swapping
middleware. For example, if you wrote your own params parser middleware, that
ActionDispatch::ParamsParser, you can swap it using:
This will swap the middleware classes, as in, it will use
Just to give you an idea, this can be useful when debugging - you can extend the existing middleware, add some logging so you can see how the data mutates in the params parsers and continue with the normal execution of parsing the params. One can enable that middleware just in development mode, so the middleware is swapped only in development environment.
That being said, lets see how we can write our own tiny middleware and mount it to an existing Rails application.
We will write a tiny middleware class that will calculate the delta time of the request and log it to the Rails console. You can open any Rails application that you have laying in your computer and play with it. I promise we won’t do anything malicious. :-)
First, we need a middleware class. Lets add it to the
directory. It’s worth mentioning that the directory does not exist by default.
So, if it is missing - feel free to create it.
Next, we’ll need to add the
initialize and the
call method. Remember, the
first argument of the
initialize method is the application, and the first
argument of the
call method is the request environment.
Next, we’ll need to calculate the total time that the applicaiton took to process the request and log it:
As you can see, this is quite trivial. We save the time before the request has been passed onto the rest of the middleware stack and the time after the middleware has finished with the request. Then, we subtract the start time from the end time and we get a number of seconds that the request processing took.
What’s cool in Rails middleware is that we have the Rails application available to us in the scope of the middleware. This allows us to use to Rails logger and log the delta time.
If you managed to add your middleware to the Rails app, go ahead and boot it. When the server boots, issue any request to it. In the logs you will see something like:
This is our
DeltaLogger logging the delta time.
Formatting the output
In one of the examples in the introduction of this post, I mentioned the option
of passing arguments to the middleware class. Lets see how we can use this
feature to improve the formatting of the
Passing arguments is done by adding the arguments after the class name in the mounting command:
In our tiny example, we can send through the character that we want the output to be formatted with. Currently, our default formatting is done with the equals sign. Making this customizable is easily done by sending this character as the first argument:
Now, we can change the output when adding the
DeltaLogger to the middleware
It’s worth mentioning that if your Rails application is running, when changing a middleware class you will have to reboot the application so the new changes in the middleware can be picked up. This happens because Rails loads the middleware only once - on boot.
Now, when we send a request in the logs we can see that the
Another way to leverage arguments is to make the logging level customizable.
For example, you might want to change the logging level. In Rails, there are
six different logging levels:
:unknown. Lets make the logging level customizable.
Now, in our application.rb (or environment.rb) file, we can set the logger to use the desired loging level:
If we reboot the Rails app and send a new request, the logs will show:
Be aware that your output may vary because logging output relies on the logger formatter that your application is using.
If you are not seeing the exact output, you can plug this logging formatter in your app:
You can use this formatter by including it into
After adding this, you’ll need to reboot your server and you should see the output with the severity tag.
Last of the important topics about Rails middleware is thread-safety. When using Puma or Unicorn web servers, one of the strong sides of these servers is that they are threaded. Since our middleware runs on these servers, it has to be thread-safe, meaning, it should easily spawn multiple duplicates of it so different threads can use different objects of the same middleware.
The easiest and most efficient way to do this is to
dup the middleware
object that is created in runtime.
By adding the
_call method and duplicating the object we make sure that
any instance variables we set in
_call will be set on the duped instance,
not the original. This allows the web server to use a separate (duped) object
for each thread which will contain different data, based on it’s execution.
As you can see, there’s quite a bit that needed to be covered in this post. I hope that this explained how you can write your own Rails middleware and mount it in the middleware stack. It’s very easy to get creative with Rails middleware as it iss highly customizable, quite low-level(ish) while you have the whole app available in runtime. Next time, we will work together on building and testing a real-life example of Rails middleware.
Until then, feel free to drop a comment, I would love to hear your opinions, problems and code that you’ve come up with.