00:00:11.960
Welcome everyone! Thank you for coming this afternoon. I hope you've had a good first day of RailsConf so far. My name is Jason Clark, and I work for New Relic, previously on the Ruby agent. Now I'm working with some other teams on some backend services.
00:00:19.410
Today, I'm here to talk to you about Rack. Rack is a library that you may have heard a little bit about. But before we dig into it, I’ve got a couple of links for you.
00:00:32.279
The first one is the slides for this presentation, so if it's easier for you to follow along on your computer or you want to reference it, that link will take you there. The second link is just a fun little Rack application that I built to demonstrate some of the principles we’re discussing. It lets you post text-based robots that will fight each other.
00:00:45.059
We probably won't get to demoing it today, but if you're like me and tend to drift a little during a presentation, feel free to go visit fightrobotlikes.com. This is meant to be a professional talk, but go ahead and have fun with that if you'd like.
00:01:03.840
Now, let’s get down to the meat of what we're here to talk about today. If you've been around Ruby and Rails, you've probably heard Rack mentioned before. But what is it? It's a bit ambiguous until you dig in and see what part it plays in the ecosystem.
00:01:18.060
The simple answer is that Rack is a gem, like so many other things in Ruby. It provides a minimal, modular, adaptable interface for developing web applications. If you're like me, that doesn’t really tell you much. However, we can draw a picture that illustrates what Rack is about and where it fits in our stack.
00:01:35.820
When a user comes to a website from a browser, they make a request. That request goes across the internet and gets handled by some sort of web server, typically something like Unicorn or Puma. This web server then figures out what to do with that request—and this is where Rack comes into play.
00:02:09.690
Rack is the interface that those web servers communicate with to signify that a web request just happened. Rack then passes that request along to a web framework of your choice, like Rails or Sinatra. So, Rack is sort of the glue that sits between your web server and your application code.
00:02:28.829
This is really cool because it means you can swap your web servers without changing your application code. You can change from Puma to Unicorn and back again, or even run on Webrick, and all of that is okay because everyone talks through this common interface, Rack.
00:02:52.470
However, Rack is also available for you to use directly. There's no reason you can't leverage what it provides to write your own code against it, instead of relying on a framework.
00:03:06.359
Now, you might wonder why you would want to do something like that. I mean, I’m here at RailsConf, and I'm telling you you can do things other than use Rails. However, there are a couple of scenarios where knowledge of Rack really comes in handy.
00:03:29.699
The first is when you need the absolute utmost speed from your Ruby code. Because the web server makes one method call into Rack to hand you a web request, there's nothing else in between. There are no additional layers, abstractions, or logic that could slow things down. If you need something to be as fast as possible, getting down to the Rack layer will strip everything else away.
00:03:50.099
The second reason is simplicity—although I have to put an asterisk on this. This is a specific sort of simplicity. The protocol for how you communicate in Rack is quite basic: it's just method calls, arrays, and hashes—things you're likely really familiar with. However, this simplicity comes with its own trade-offs.
00:04:02.879
While it may simplify some aspects, it might complicate others, and we can talk about where that’s the case. Another nice feature of Rack is the high degree of reuse of some of its components, because Rack is such a standard part of the Ruby web ecosystem.
00:04:27.150
Things written to work with one Rack-based application can often work with another, or even with applications written against entirely different frameworks. So, there are nice advantages if you're writing against Rack that you may not have when creating code as a before filter in your controller.
00:04:56.010
Alright, that’s a lot of yammering about what Rack is. Now, let’s take a look at some code. This is actually one of my favorite snippets of Ruby.
00:05:10.320
This is a fully functioning Rack web application. You would typically put this in a config.ru file. Don't let the .ru fool you; it's still just a Ruby file. The .ru stands for Rack Up.
00:05:21.990
We define a class called Application, and in that class, we define one method. The entirety of Rack's protocol is a single method, called call. It takes one parameter called env, which is a hash. We’ll look closer at what's in that hash shortly.
00:05:37.229
On the response side, Rack expects the call method to return an array with three parts. The first value in that array is the HTTP status code to return. In this case, we are saying 200, which means everything is okay.
00:05:52.289
The second parameter in this array is a hash of HTTP headers that will be sent back to the client browser. I've left this empty for now; typically, you would include something like the content type or some basic values.
00:06:07.020
However, any HTTP header you need to communicate back to the client can simply go in that hash. The last part of this three-part array is the content you want to send back.
00:06:25.320
This is slightly different from what you might expect. You might think you’d just want to hand a string back—like your HTML or the JSON response. However, Rack expects the last content object to respond to 'each.' So, using an array is the easiest way to achieve that.
00:06:39.690
The server will take whatever comes out of that enumeration and send it down the wire to clients. So, that’s it! That’s the protocol by which the web server communicates to Rack for the incoming request and gets the response back from the Rack application.
00:07:13.889
In config.ru, we need to get the server started, and there is a run method that Rack provides at that level. You pass it an instance of a Rack application, an instance that responds to this call method.
00:07:26.069
When we go to our terminal, we can say 'rackup.' That is an executable installed by the Rack gem, and it will output some front matter, telling us about versions and what port number it's running on. Now it’s ready to receive requests.
00:07:50.550
We can access it in the simplest way by going to our browser and entering localhost:9292. It informs us that this is all good.
00:08:09.750
There are other ways to access this, though. I like to experiment with them because some later cases will be easier with different tools.
00:08:16.340
One is curl, a command-line tool readily available across most UNIX-based systems.
00:08:21.449
You can use curl in the most basic way by typing curl followed by the same address you would have typed into your browser. This command will print the response right back in your terminal.
00:08:38.520
There are all sorts of flags, but we’ll only focus on a couple for now, as they’ll come in handy later when we explore more complex scenarios that cannot be easily typed into a browser.
00:09:01.890
As Rubyists, a significant part of Ruby culture is testing. We have an app ready, so let’s see what it would take to get some tests wrapped around it. Unfortunately, as you might expect, the community provides a gem called Rack Test.
00:09:20.220
You include this in your Gemfile, just like any other gem, and require Rack Test to make it available. This pulls in the classes you'll be using in the examples I’m about to show.
00:09:35.980
I'll be showing many tests because that's my preferred style. This works seamlessly with RSpec and is well-supported across that testing framework.
00:09:57.980
A test for our application would look something like this: we include the Rack Test method, which provides a whole bunch of helpers that will be available throughout the rest of your test class.
00:10:17.050
You can do this for everything if you know you're going to be using it everywhere, or you can apply it point by point like we're doing here. One expectation that Rack Test has of your test class is that it will provide a method called app.
00:10:35.980
This app method should return an instance of your Rack application. This allows us to control what we are testing directly. The methods and helpers that Rack Test provides will interact with this app method to perform their operations.
00:10:54.410
As we’ll see later, this setup plays well because you can also do things like instantiate middleware or configure as needed to return a valid Rack application to test.
00:11:12.760
The tests themselves are quite straightforward, providing methods that allow you to invoke the typical HTTP verbs you would expect. For instance, if we simply want to 'get' what's at the root, we just call get '/'. When that is executed, it runs against our Rack app.
00:11:32.380
Rack Test will store the responses from the app in an object called last_response, allowing us to interrogate it for things like the status code, the content that was returned, and the headers.
00:11:54.690
You can see how this enables a nice unit testing cycle, where you can feed various inputs, make requests against your Rack app, and analyze its responses.
00:12:06.180
Now, while this is all well and good, I’m sure someone would pay you a lot of money to create a web app that simply returns a static string. However, most of us will require some form of more dynamic, interactive behavior.
00:12:34.360
And that parameter we looked at, 'env,' is crucial for all of the dynamic behavior you’ll want to incorporate into your application. Everything you need to know about the incoming web request is wrapped in that 'env' hash.
00:12:46.790
Let’s examine some of the important information contained in there. It includes the HTTP request method, so in our case this is a 'GET' method. This method is the one sent to the server when you type something into your browser and hit enter.
00:13:01.990
Additionally, it provides various path information, allowing us to know what was requested and where. It also wraps up any input that's coming in, though with a 'GET' request, there's no real input being sent, just the requested URL.
00:13:32.270
However, with a POST request, you will send data along with the request, and this will be accessible through the 'env' hash.
00:13:39.490
Now, let’s modify our basic app to start digging into 'env' and implement some more complex functionality, although we’ll keep it simple for now.
00:13:55.160
We’ll check the path information that was provided in the 'env' hash, specifically looking to see if it matches '/bot.' If it does, we’ll return a 403 HTTP status code for forbidden access.
00:14:19.890
We’ll also include a nice little message to let the user know that we've detected their attempt to access a restricted area.
00:14:36.310
This process of examining the incoming URL and translating that into executable code is a fundamental aspect of web architecture, often referred to as routing.
00:14:56.800
In fact, many web frameworks tout their routing capabilities, making it easy to manage and configure routes cleanly. That said, when you take a bare-bones approach using Rack, you’ll likely find yourself creating your own abstractions to avoid deeply nested if statements.
00:15:15.490
While it won't get too complicated with a small amount of routes, imagine dealing with ten or more routes without a structured way to handle them. It could become quite challenging.
00:15:40.430
However, Rack does offer some assistance in this area. It allows you to provide leading prefixes to URLs and send calls to specific application objects, which helps you manage routes.
00:15:56.840
In our config.ru, where we initially called run on the main application, we can also support routing prefixes by creating a new Rack-compatible application object called status.
00:16:18.390
This status app will now receive calls meant for the '/status' URL while ensuring that they don't pass through the main application object.
00:16:31.220
If you have more than one or two routes, Rack provides a URL map class that lets you hand it a hash of prefixes and their corresponding application instances. Depending on how many you manage, this can be a more organized approach than standard map calls in your configure.ru.
00:16:56.080
The 'env' hash contains all possible request data, but it can be messy to work with raw hashes. That's where Rack helps us with the Rack::Request class.
00:17:04.420
You pass it the incoming 'env', and it provides a lot of helper methods that give you cleaner access to what's expected inside that Rack request.
00:17:19.410
For example, instead of referencing 'env['PATH_INFO']' directly, you can use the 'path_info' method, and if you make a typo, it’ll throw a no-method error rather than just returning nil.
00:17:32.840
I recommend using Rack::Request whenever you're doing more than a couple of straightforward accesses from 'env', as it provides a robust abstraction.
00:17:50.630
Let's take this a step further. Instead of simply returning a static string from '/bot', let’s make it more like a Rails show route with a dynamic ID.
00:18:07.990
We’ll still return a forbidden status, but we’ll indicate what user ID they were attempting to access. While this approach might not be the cleanest, it effectively illustrates the routing possibilities within Rack.
00:18:20.799
That being said, using regex for routing can be cumbersome. While it's compact, if you want more user-friendly or elegant routing, frameworks like Sinatra provide DSLs that allow you to define routes with much less boilerplate.
00:18:38.079
When we match a route successfully, instead of just returning a static string, we can now execute any kind of code we desire. For example, we can set up a fictional database class that looks up items by ID.
00:18:54.699
This class returns the corresponding bot object that we can then render as our output.
00:19:09.740
Now we can even curl against bot/1, which will interact with our imaginary database and return a visual representation of the object with ID 1 as the response.
00:19:31.020
In this case, we’re dealing with an HTTP GET request, so no additional parameters are expected apart from the URL and query strings. You could add headers, but we’re not reading any here.
00:19:51.890
Let’s transform this application to support POST requests, allowing users to write data into our database. For that, we simply adapt our call method a bit further.
00:20:05.120
Don’t worry; this is as complex as our method will get. Here, we check if the request is a GET method. If it is, we proceed as we did before.
00:20:25.010
However, if the request is a POST, we read the request body to get the data being sent from the client.
00:20:42.550
This data often originates from web forms but can also come directly from API clients. After capturing the content, we proceed to write it into our database as needed.
00:20:55.620
We can then return a response to the client, indicating that we successfully saved the record they sent. This is effectively how a basic RESTful API is structured.
00:21:13.820
This example shows that building simple yet functional applications is indeed straightforward in Rack.
00:21:28.890
Once you get the hang of it, you can see how removing excessive frameworks simplifies the model.
00:21:40.120
To test this lifecycle, you can use curl with '-X' to specify HTTP methods other than GET, providing the full URL with the ID of the item you want.
00:21:58.390
Using '--data' lets you send the necessary data along with the POST request, demonstrating the full process of sending and receiving data.
00:22:12.780
Unsurprisingly, since Rack has a request class, it also has a response class that helps in constructing valid responses. This class is pretty useful.
00:22:39.230
For one, it ensures the response is formatted correctly without enforcing the strictness of producing exactly those three components every time.
00:22:54.580
Additionally, Rack::Response allows for a more fluid approach in building responses rather than just returning the array inline. You can set various properties incrementally.
00:23:07.330
This means you can create more readable and maintainable code, especially if responding with multiple dynamic pieces of content.
00:23:21.660
When you’re done setting up your response, you simply need to call finish, which generates a valid Rack response to be handed back across the wire.
00:23:34.240
If you print the response and look at what it returns, you'll find it also maintains the three-part array structure: the status code, headers, and body.
00:23:51.380
However, with Rack::Response, you'll notice that it also sets the content length based on the content you added—something browsers may not immediately break for, but it’s definitely in the HTTP specs.
00:24:06.410
Moreover, the content you previously sent as an array of strings will be encapsulated in an object, allowing Rack to manage any intricate scenarios more effectively.
00:24:23.420
Now that we have a valid app returning data, we can interact with it and make requests to receive responses. However, an important aspect of how Rack functions is middleware.
00:24:43.650
This pattern is incredibly powerful and frequently leveraged in other frameworks. In our config.ru, where we previously called our application, we can also use a method called use to install middleware.
00:25:01.650
When you install middleware, they get activated in the sequence they were set, creating a call chain for requests. The first middleware processes the request, followed by subsequent middleware, all leading to the app.
00:25:19.020
Responses follow the same route back through the chain, allowing you to handle pre-processing and post-processing of requests and responses.
00:25:37.060
Here’s an example of a basic middleware that checks for an API key. If the provided key matches a predetermined string, the request will pass through; otherwise, it returns an error response.
00:25:50.660
Visually, it looks like this: the middleware reads and validates the API key. If it’s valid, the request continues; if not, it stops right there.
00:26:08.600
Middleware is advantageous for applying cross-cutting concerns like logging or authentication. These behaviors can smoothly be integrated into multiple endpoints across your application.
00:26:26.230
Rails is built massively on middleware, utilizing this pattern extensively for straightforward behavior across various application aspects.
00:26:46.580
Once the authentication middleware is installed, we can observe the result of our operations. If we try a request without a valid API key, we will receive a forbidden message.
00:27:03.030
However, if we do provide a valid API key using the HTTP header format, the request is eventually allowed through.
00:27:18.300
Rack comes equipped with numerous middleware components, including rack-static for serving static files and session support, both server-backed and cookie-based.
00:27:34.170
It also provides debugging tools, such as exception pages in development, alongside code reloading features, which bring many comforts often seen in Rails into Rack.
00:27:50.130
That leads us to our final discussion: how Rack intersects with various frameworks.
00:28:07.110
We noted earlier that Rails and Sinatra and others are built atop Rack, but what does this mean in practical terms?
00:28:25.490
In a Rails application, it is a valid Rack application. It has its own call method that processes incoming requests, dispatching them to controllers, routing, and views.
00:28:41.810
But Rails does all this for you, so you don’t need to write a custom call method yourself.
00:28:54.640
You can also embed Rack applications directly inside a Rails application using the mount command, delegating to that Rack application for specific URL paths.
00:29:08.660
Rails also permits the use of middleware similar to how we handled in our config.ru. You can utilize the use method in the application config to introduce middleware.
00:29:25.390
You can also strategically control middleware sequencing with 'insert_before' and 'insert_after' commands, something you cannot do directly in Rack.
00:29:43.520
That said, Rails has its own internal middleware stack. When you configure the use of middleware in a Rails application, it interacts with that internal stack, which won't show up in Rack’s middleware list.
00:30:02.050
This distinction can be a little tricky when searching for installed middleware, as it won’t show up if you try to query with something like 'rake middleware'.
00:30:19.080
Sinatra operates similarly to Rails; at the end of the day, when you call 'Sinatra::Base' and derive your class from it, you’re creating a Rack application.
00:30:37.120
While you’ll normally use the helpers and output strings easily, you also have the option to return a valid Rack response directly, as it’s fundamentally just a Rack app.
00:30:56.760
In conclusion, we’ve gone over the core aspects of Rack and how to build simple applications, examining the integral components such as request handling, response generation, and middleware.
00:31:12.210
We've also explored how Rack fits within various frameworks. I’ve barely scratched the surface, and for those interested, I’ve created a course for Pluralsight.
00:31:32.650
I have several tokens available for a free month of access to Pluralsight, where you’ll find more detailed content explaining Rack.
00:31:49.680
I hope you’ve found this talk useful, and I’d love to hear any questions you may have.