00:00:10.759
Good morning, everyone! Thank you for coming to this talk. My name is Amy Unger, and I'm here to discuss a part of the Rails ecosystem that I ignored for quite some time, and I came to regret that. It's easy as a new Rails developer to overlook Rack middleware. I mean, it has words like "Rack" and "middleware," which can be intimidating.
00:00:18.080
Especially when you're still getting comfortable with the concepts of models, views, and controllers. As I became a more experienced developer, I ran into gems that relied on middleware and even encountered small pieces of middleware in the codebases I was working in. It’s really easy to focus only on the parts of the middleware that seem relevant to the changes you need to make or the bugs you’re trying to track down. So, it’s pretty easy to assume that there’s not much more to Rack middleware. To be honest, there isn’t. It's designed to be a simple but powerful interface.
00:00:50.680
However, I never took the time to truly understand what was going on. At the beginning of my career, Rack middleware seemed far too advanced for me. Later, I found it to be boring and too obvious. Here’s a secret: I wrote some pretty bad middleware because of that. I wrote middleware that wasn't thread-safe, didn't push back when middleware that should never have been written was created, and I maintained sprawling middleware that was practically unintelligible. Finally, I didn't write middleware when I should have. I simply didn’t know that it was a tool I could use.
00:01:20.520
So today, I want to discuss some things that I wish I had known and some mistakes I made so you don’t have to repeat them. First, I'll explain what Rack middleware is and then go through some examples of how we can build Rack middleware. I’ll cover why you might want to use middleware as a tool. Finally, I’ll conclude with a section titled "Who?" discussing who might have made similar mistakes.
00:01:44.840
Let’s jump right into what Rack and Rack middleware are, look at how they fit into Rails, and then take a brief look at some familiar examples of Rack middleware. So, what is this Rack thing? Let’s consider a world where Rack doesn’t exist, and you need a server, for instance, using a CGI server. In this scenario, your user makes an HTTP request through a browser, and it hits your CGI server.
00:02:31.560
The CGI server takes that request and parses it into different parts of data, shoving those into the environment, which consists of around 20 or 30 environment variables. Some of these should be somewhat familiar: path info, HTTP headers, and so forth. This information is then processed, and your application code writes to standard output, which the CGI server picks up to form an HTTP response and send it back to your user.
00:02:48.439
Now, what this means is that your application needs to know it's being run by a CGI server. It has to extract those environment variables to figure out, for instance, that someone is requesting index.html. This can be problematic since we often develop with one server and then deploy to production using another. Your application has to adjust to whatever server it’s running on.
00:03:18.640
Now, let’s move into a world with Rack. When we introduce Rack between your server and your application, the situation changes. Your user makes an HTTP request to your server – it could be Webrick or any supported server. The server knows it should communicate with Rack.
00:03:35.400
Rack then parses the information it receives from the server into a standard incoming request that is consistent across different servers. This means no matter which server is running your application, your app can write the same logic. When responding, it’s the same process. Your application returns the response in a Rack-compliant way, and Rack figures out how to communicate with the server.
00:04:01.440
To your application, Rack presents an incoming request with an environment hash. Rack took inspiration from CGI as it essentially wrapped those environment variables in a hash, calling it the environment. However, it does not set those variables in the environment; instead, it passes them into your code. The outgoing response from your app includes the status code, headers, and content body.
00:04:47.679
So, let’s examine the simplest Rack app we can create. Rack apps need to follow three rules: we need something we can call, whether it’s a class, an instance method, or the proc we have here. That entity must accept the environment, which is a hash including path info, headers, and all the relevant data. Then, it needs to return an array with the following components: the status, a hash of headers (for instance, returning HTML), and an array of the content body.
00:05:14.440
Thus, those are the three rules that need to be followed to qualify as a Rack app. So, what is Rack middleware?
00:05:30.920
If we look at this diagram and zoom in on Rack, we see three parts to working with Rack. Rack took its logo inspiration from a server rack, and I’m going to use that concept to illustrate these three parts. The first part is the handler for your server (like Webrick, Mongrel, CGI, Puma, etc.). There’s a handler for each.
00:06:00.400
The next part is the adapter at the bottom that communicates with your framework. Initially, the setup is simple; the request just flows through Rack and gets transformed for both the server and the framework. But what if we wish to intervene in that request before it reaches the server or the application?
00:06:19.480
That’s where middleware comes in. Middleware allows us to manipulate the request or response before it exits out the bottom of the stack or comes out the top.
00:06:28.480
To illustrate the power of what Rack middleware can accomplish, let’s look at some examples of middleware that Rails provides. First, Rails uses middleware to serve static files, to set up logging for each request, and to flush all logs at the end of the request. Middleware is also used to set cookies for handling flash messages and parsing params. If you’re familiar with the params used in controllers, that’s handled in middleware.
00:06:44.640
In addition to middleware in the Rails core codebase, other notable gems in the Ruby web app ecosystem utilize middleware as a strategy. Examples include throttling for security with Honeybadger and authentication with Warden. Now, let’s take a quick look at how we can build our middleware.
00:07:14.760
First, we'll write a basic middleware. This will serve as a simple ping setup; basically, we’ll ask our application, "Are you up and running?" We’ll create a file in `lib/middleware/ping`, and then we create a class called Ping. To prevent any namespace clashes, we’ll place that class within a module.
00:07:35.240
Now we have the Ping middleware class. We’ll write an `initialize` method, which accepts the app. This app could refer to your Rails app, but it could also refer to another piece of middleware. You can imagine middleware as a set of Russian nesting dolls, each calling down to the smaller one until it hits your application. Essentially, our middleware gets initialized with the next middleware down the stack.
00:08:07.160
The next thing to remember is that any middleware needs to follow the same three rules as an app. It must respond to `call`, accept the environment, and return the status, headers, and content body. Our `call` method is going to resemble a Rails app or any Rack-compliant app, except it will also need to call down the stack.
00:08:25.040
Let’s write the simplest Rack-compliant response—our `call` method here accepts the environment and returns a Rack-compliant response. While this may be cool, it will always respond with "pong." This, of course, isn't what we want to achieve, so let’s fix that.
00:08:58.560
First, we will take a look at the request, parsing out the environment into a more manageable format. The request will have environment variables set, allowing us to see if it's a GET or a POST request, and we can also examine the path info. With this information, we complete the method: if the request path matches the route we want, we respond with a 200 and "pong." If not, we just call down the stack and pass everything down.
00:09:31.760
Next, let's explore a more complex middleware example: request response time logging—which is one of the most common middleware functionalities. It’s remarkable how often you don’t have access to nice tools like New Relic for monitoring, instead finding yourself needing to implement your logging and monitoring.
00:10:01.720
In this case, we're going to track how long it takes your app to fulfill a request to index.html or any route. We'll follow the same pattern we used for the Ping middleware to create a new file, `lib/middleware/request_time_logging`. We'll structure it with a class again and create the `initialize` method, which will carry a reference to the app down the stack.
00:10:36.400
Now we come to the core of this middleware—the `call` method. This method will take in the environment. We know that we must call down the stack for this middleware, as it won’t drop or intercept any requests. We need to record the start time of the call and then compute the elapsed time.
00:11:05.440
However, the issue is we’re currently just returning the elapsed time in seconds, which isn’t a Rack-compliant response. We want to return the status, headers, and response. We’ll save that data as we call down the stack and, as we get that response coming back, we can return it.
00:11:27.120
Even though our app is functioning well with users hitting various endpoints and getting responses, we still need to log the elapsed time somewhere. To achieve this, we’ll create another method called `log_response_time`. This method will take in the elapsed time and the request, allowing us to create a JSON payload with that data to log it.
00:12:05.760
Because sending this logging information is most commonly done to tools like Splunk, we can write an arbitrary implementation to send our payload with the request and response time. And just like that, our request response time logging middleware is complete.
00:12:38.920
Now, let’s quickly review an example of middleware in a gem. This is often useful when you’re debugging something, as it’s beneficial to deal with a larger codebase. A great example is throttling middleware, which can drop requests or handle them efficiently, returning unauthorized responses without putting undue load on your application.
00:13:30.320
In many cases, you’ll see a GitHub repository for Rack Throttle, and one interesting observation when debugging middleware gems is the need to locate the core middleware class—the one with the `initialize` and `call` methods. This class often has a different name; in this case, it's "Limiter." Once you identify it, you will discover the methods taking the app as well as options that allow flexibility for user configuration.
00:14:01.760
The call method, of course, takes the environment, triggering relevant logic to determine if the request is allowed. If it is, it calls down the stack; otherwise, it issues a method indicating that the rate limit has been exceeded, likely returning an unauthorized response with a helpful message. This demonstrates how little Rack middleware code you need to write to create effective middleware.
00:14:41.800
So, why would you want to write Rack middleware? Middleware can simplify your application; it's great for managing requests your app shouldn’t see. For example, at my workplace, Heroku, we used to support a website called add-on.heroku, which acted like a catalog of products. Behind this application exists an admin interface for managing those add-ons, but the product data has since moved.
00:15:19.600
Now, many users still attempt to access those add-on routes, but without any related front-facing functionality. The route files became extensive, requiring hundreds of lines to redirect users seamlessly. To manage this better, we moved some routes into middleware, making the application simpler. This way, when users accessed old routes, they didn't hit the bloated route file, and developers had less to manage.
00:15:56.800
Middleware can also protect your application. Continuing on the theme, there are certain requests you don't want to reach your app—especially malicious requests. Middleware can intercept these requests; for instance, throttling examples can be implemented so that requests your application shouldn't see are blocked, thus preserving your server resources.
00:16:34.280
Moreover, middleware can also implement honey pots. Since middleware has access to both the request and response objects, straightforward logging methods would only observe the incoming request and the result afterward, while middleware can access both simultaneously. This is why implementing features like the request/response timer inside middleware is advantageous.
00:17:11.640
Another key advantage of middleware is its ability to serve as a code-sharing mechanism. If you’re hoping to share certain aspects of your code as a gem, middleware provides an effective route for doing so. Users can easily integrate this functionality with minimal setup.
00:17:55.040
Lastly, I want to discuss the things that can trigger your successors to critique your code. First, the order of middleware is important, much like the Russian nesting doll analogy. You cannot fit the largest doll into the smallest one; similarly, the order of middleware in Rack matters critically.
00:18:48.520
Rails provides a handy rake task to show the configured middleware for an application and the order in which they’re executed. If you run `rake middleware` on any modern Rails app, you'll see the output indicating how your Rack middleware will run as requests come in and in reverse order as responses exit.
00:19:25.960
Let's explore how order can create issues, using return statements for static file requests as an example. Rails is set up to respond immediately for static files at the top of the middleware stack, while request IDs and logging are set below. This causes static file requests to go unnoticed in logging unless you have alternative monitoring systems or change the order so that static file requests are logged.
00:20:11.640
Consider the case where we add Warden right from the top of our middleware stack. While that might seem appealing at first, the Warden library documentation states that it must be downstream—meaning it depends on session variables being set in prior middleware. Thus, we must ensure Warden is lower in the stack to work correctly.
00:20:55.000
As noted earlier, Rack middleware is a fantastic tool to simplify your application by extracting irrelevant parts. However, nothing prevents you from improperly obscuring your entire app through middleware. If that happens, debugging becomes complicated, and it's better to have a clear structure where developers can locate logic.
00:21:34.360
Some red flags to watch for when considering whether or not to place logic in Rack middleware are modifying request data. If your middleware modifies or overwrites request attributes like post data or request paths, you’re likely proceeding down a troublesome path. It’s also crucial to have awareness of business logic since searching for bugs across multiple pieces can be hard.
00:22:23.720
You also want to avoid placing business logic in middleware, primarily because it will be more challenging to test, leading to issues later on. Awareness of different data structures is vital; if the middleware knows too much about the model or data, it may tie your application together unexpectedly.
00:22:58.240
To mitigate these issues, I suggest using app middlewares to clarify that you're implementing significant logic. This contributes to easier debugging since another developer can quickly search for keywords in the app. If your middleware is particularly hacky, use 'app hacks' or 'lib hacks' as a warning flag; this indicates a lack of comfort with what you’re doing.
00:23:51.160
Lastly, always ensure that your middleware methods return a response. Remember that you’re in a stack of middleware, and adhering to the three rules of Rack is crucial. If your middleware throws uncaught exceptions, that typically results in a 500 error, which isn’t pretty. You will not receive the friendly Rails error page, as your Ruby code will have errored out.
00:24:36.640
Finally, we should discuss thread safety in the context of Rack middleware. This concern is primarily relevant if you're setting instance variables. Such practices typically indicate a more complicated piece of middleware than what I’ve presented today. As an example, we’ll make our ping middleware thread-safe—even though it technically doesn't need to be.
00:25:29.720
To achieve this, we’ll duplicate the instance of middleware, transferring all the logic found in the call method to a new private method, conventionally named 'uncore_call', and that’s it!
00:26:07.240
To summarize, we’ve reviewed what Rack and Rack middleware are, along with some reasons to utilize middleware in your application. We also discussed useful practices for ensuring you use middleware wisely. I hope this talk has energized you about possibly employing Rack middleware in the future. Thank you!