Last time I wrote about the basics of Rack and writing a tiny Rack application. If you are unsure what Rack is and what is it’s purpose, I recommend you read the other post, famirialize yourself with Rack and get back to this post. If you think you know enough about Rack, please, carry on reading.
So, middleware. Lets take it from the basics. What is middleware? Remember that Rack “wraps” HTTP requests and responses? Also, remember that it has this interface (or API) which allows you to play with the request and the responses?
Well, middleware is a term that is used when speaking for a piece of software which is somehow related to the execution of the web application, but is not directly involved with the execution of the requested task.
Confusing? Okay, lets put it in another way.
Think of logging. You request a resource from an API, and the API responds with your results. Although you did not request the API to log your request (why would you?), the API has a logger which records everything that goes in and out of the application. So you see, although the logger is not directly involved with handling your request, the logger is middleware that writes down what you requested and what the API responded with.
How it works
Every Rack application is a class. The same applies to Rack middleware. Lets write the class and see some basic rules that apply to writing middleware classes.
When writing a Rack middleware class, the first argument of the
is the application, or the request handler. If that’s confusing, think of it in this way
- the request handler has to be passed in the middleware class, so the middleware can wrap the execution of the application and do something to the request and the response of the application.
Another rule that applies to a middleware class is the
call method. The
call method executes the application which returns the status, the headers and the
body of the response.
Take this for an example:
When we run this file with
rackup test_handler.ru, the WEBrick server will boot
and our request handler will be served (your output may vary):
When you visit localhost:9292 in your browser (or cURL it), you will see the “Request handled.” message. But the server logs are what we are interested in:
As you can see, the app is actually the handler, or, the
HandlerClass. It only has one method, the
call method. But, what’s really interesting is the order in which the statements are executed.
MiddlewareTwo wraps the request and prints the app and it’s methods. After,
it executes the
@app.call(env) line, which is the
When it calls the
call method, it actually executes the
MiddlewareOne call method.
Then, that method prints out the app and it’s methods. Here, the app is the
After it calls the
call method, the response is served.
This is what the wrapping looks like:
This means that
MiddlewareOne which executes
Let’s take it to the next level and write our own logging middleware. We’ll call the class Loggster! :-) What we are aiming for is middleware, that will wrap our Rack application and will log the requests that come in to a logfile. Sounds simple? Lets see…
The Rack application, or, the
RackApp class just returns a HTTP 200 with a “Hi!” message. Simple stuff.
So, what does Loggster do? When we add the
use Loggster line in the file, we tell Rack to wrap
the request handler a.k.a.
Loggster. This means that
RackApp.call, it will write to the log file and return the full response that
The “magic” call method, is really simple. It checks if there’s a directory called
“logs” and creates it if it does not exist. After, it creates a
file and inside, it writes the current time, the request method (i.e. GET), the
URL that the web client requested and the response HTTP status.
If we run this file with
rackup loggster.ru and visit the URL, we’ll see that a directory called
“logs” and a file “server.log” inside, have been created. So, how does the output of our simple logger looks?
As you can see, I requested
localhost:<port-number>/something via my browser. The browser requested the path
and it also requested the favicon twice.
As you can see, writing Rack middleware can be pretty simple. Of course, it all depends on what functionality you would like the middleware to have. That being said, the rules (or constraints) that Rack imposes when writing middleware are tiny and very clear. In the next post, we will see how we can implement Rack middleware and mount it in a Rails application. In the mean time, did you implement any Rack middleware yourself?
Feel free to share your stuff (or ask for help, if needed) with me and my readers in the comments.