#317 Rack App from Scratch pro
- Download:
- source codeProject Files in Zip (3.81 KB)
- mp4Full Size H.264 Video (29.8 MB)
- m4vSmaller H.264 Video (16.4 MB)
- webmFull Size VP8 Video (19.8 MB)
- ogvFull Size Theora Video (33.4 MB)
Rack is mainly known as a way to build Web applications in Ruby, but the Rack gem also includes a lot of useful utilities many of which Rails uses internally. If we want to build a lightweight Web app we’ll generally use a framework such as Sinatra but by using Rack directly from scratch we can learn a lot about these various utilities.
Getting Started
To start we’ll install the Rack gem to make sure that we have the latest version installed (1.4.0 at the time of writing).
$ gem install rack
Next we’ll create a new directory for our application, which we’ll call greeter
and move in to.
$ mkdir greeter $ cd greeter
To make a Rack application all we need is a Rackup file.
class Greeter def call(env) [200, {}, "Hello, World!"] end end run Greeter.new
In this file we’ve written a Greeter
class. All Rack applications need to have a call
method that takes an environment hash variable as an argument and returns an array containing a numeric HTTP status, a hash of headers and a response object and that’s what we’ve done here, although our header hash is empty. Finally we run a new instance of the class.
We start this app up by running rackup
.
$ rackup >> Thin web server (v1.3.1 codename Triple Espresso) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:9292, CTRL+C to stop
This will start the application up on its default port, 9292. Let’s take a look at it in a browser.
When we do we see a Rack::Lint::LintError
. Rack::Lint
is one of the utilities that Rack provides and this error is shown because our application isn’t returning a Content-Type
header. Where does this error page come from? When we run the rackup
command the Rack::Server
class’s start
method is called and this includes several pieces of Rack middleware by default.
def self.logging_middleware lambda { |server| server.server.name =~ /CGI/ ? nil : [Rack::CommonLogger, $stderr] } end def self.middleware @middleware ||= begin m = Hash.new {|h,k| h[k] = []} m["deployment"].concat [ [Rack::ContentLength], [Rack::Chunked], logging_middleware ] m["development"].concat m["deployment"] + [[Rack::ShowExceptions], [Rack::Lint]] m end end
One piece of middleware that’s included is Rack::ShowExceptions
and it’s this that captures exceptions and creates the formatted output to send back to the browser. Another is Rack::Lint
which makes sure that our Rack application responds properly. Rack::ContentLength
, which sets the Content-Length
header, is also included as is Rack::Chunked
which handles streaming responses. It also uses the Rack::CommonLogger
middleware to log the request to $stderr
.
So now we know where the Rack::Lint
exception comes from and why the error message is formatted that way it is. Let’s fix the error by setting the Content-Type
header.
class Greeter def call(env) [200, {"Content-Type" => "text/plain"}, "Hello, World!"] end end run Greeter.new
We’ll need to stop and restart our rackup
server for the change to be picked up but when we do our application should now work.
If you get a blank page when you try this and you’re running Ruby 1.9 it’s because the third part of the response needs to be able to respond to to_each
and, while strings in Ruby 1.8 do they don’t in 1.9. The easiest way to do this is to turn the string into an array with the string as its only element.
class Greeter def call(env) [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] end end run Greeter.new
Getting Changes To Reload Automatically
There’s a lot more we want to do inside the Greeter
class but before we do we’ll organise our code and move the class into its own file in a new lib
directory.
class Greeter def call(env) [200, {"Content-Type" => "text/plain"}, ["Hello, World!"]] end end
We can then require this class in our Rackup file.
require "greeter" run Greeter.new
Now that our Greeter
class is in its own file we’ll look at another piece of Rack middleware called Rack::Reloader
. This automatically reloads any required files each time we make a request which means that we no longer have to restart the rackup
server every time we make a change to Greeter
.
require "greeter" use Rack::Reloader, 0 run Greeter.new
The reloader has a ten-second cool-down period by default but we’ve changed it to zero so that changes are picked up immediately. When we start the rackup
server now we’ll need to include the lib
directory so that it can find the Greeter
class.
$ rackup -Ilib
If we change our Greeter
class now and reload the page in the browser the change will be reflected straight away. In more complex scenarios the reloader may not work very well but in these cases there are other utilities can that help to do this such as Shotgun.
Using Template Files
Writing out the full response every time isn’t very convenient, especially if we need to send back a large number of headers. Rack comes with a class called Response which makes this easier. We can pass a body to it and it will default to a 200
status.
class Greeter def call(env) Rack::Response.new("Hello") end end
When we reload the page now the text has changed. It’s also displayed in a different font and this is because Rack::Response
uses a default Content-Type
of text/html
, rather than the text/plain
that we were using before.
Normally we just want to put some HTML into the response. Writing long strings of HTML is rarely fun and its generally better to render out a template that uses, say, erb. This isn’t difficult to do; we just need to require erb
, create a template and use the ERB class to render it.
class Greeter def call(env) Rack::Response.new(render("index.html.erb")) end def render(template) path = File.expand_path("../views/#{template}", __FILE__) ERB.new(File.read(path)).result(binding) end end
Our new render
method takes the name of a template as an argument and looks for a file with that name in a views
subdirectory. We then call ERB.new
and pass it that file. We call result
on this and pass in the current binding so that we have access to all the methods defined here. We’ll create that index.html.erb
file now and put some static HTML into it.
<!DOCTYPE html> <html> <head> <title>Greeter</title> <style type="text/css" media="screen"> body { background-color: #4B7399; font-family: Verdana; font-size: 14px; } #container { width: 75%; margin: 0 auto; background-color: #FFF; padding: 20px 40px; border: solid 1px #black; margin-top: 20px; } a { color: #0000FF; } </style> </head> <body> <div id="container"> <h1>Hello World!</h1> </div> </body> </html>
When we reload the page now we’ll see our template rendered.
Changing The Response Based on The Request
Currently our application will always respond in the same way but we want it to behave differently based on what the user requests. We can do this by using a Rack::Request
object and passing it the environment.
class Greeter def call(env) request = Rack::Request.new(env) case request.path when "/" then Rack::Response.new(render("index.html.erb")) else Rack::Response.new("Not found", 404) end end def render(template) path = File.expand_path("../views/#{template}", __FILE__) ERB.new(File.read(path)).result(binding) end end
The request
object that you’ll be familiar with in Rails inherits from Rack::Request
and the methods we can call here are very similar. For example we can call path
to get the path of the URL that was requested. We’ve done that here in a case
statement so that if the root URL is called our template is rendered. For any other URLs we’ll return a 404
response. Visiting our application’s root URL now will show the template as before but if we visit any other URL we get the 404
.
Using Forms and Cookies
Let’s say that we want a way for the user to be able to change the name they’re greeted by. We’ll need a form field where they can enter their preferred name so we’ll add a form to the template.
<body> <div id="container"> <h1>Hello World!</h1> <form method="post" action="/change"> <input name="name" type="text"> <input type="submit" value="Change Name"> </form> </div> </body>
This form will send a POST request to the /change
path and will send a name
attribute containing the text that was entered in the text field. We need to respond to this request in our Greeter
class and for now we’ll just return the name that was entered.
def call(env) request = Rack::Request.new(env) case request.path when "/" then Rack::Response.new(render("index.html.erb")) when "/change" then Rack::Response.new(request.params["name"]) else Rack::Response.new("Not found", 404) end end
The params
hash we’ve used here works very similarly to the one Rails uses except that the key needs to be passed in as a string instead of a symbol. If we reload the default page now and enter a name on the form we’ll see that name when we submit the form.
When the form is submitted we want to do more that just show the name. Instead we want to set a cookie and redirect back to the home page. To do this we’ll change the call to Rack::Response
that’s called for /change
requests so that it uses a block. This block takes a response
object and there are a variety of things we can do with it this object. For example we can use square brackets to assign header values. We won’t do that here, instead we’ll use set_cookie
set a cookie containing the name that was entered in the form and redirect
to redirect back to the home page.
class Greeter def call(env) request = Rack::Request.new(env) case request.path when "/" then Rack::Response.new(render("index.html.erb")) when "/change" then Rack::Response.new do |response| response.set_cookie("greet", request.params["name"]) response.redirect("/") end else Rack::Response.new("Not found", 404) end end
Now when we enter a name in the form and click “Change Name” we’ll be redirected back to the home page. We’ll change our template so that it displays the value set in the cookie through a new method called greet_name
.
<h1>Hello <%= greet_name %>!</h1>
We’ll write this method in our Greeter
class. It will return the value in the cookie or, if that’s not been set, default to “World”.
def greet_name request.cookies["greet"] || "World" end
This won’t work, however, as we don’t have access to request
outside our call
method. We’ll change this to be an instance variable so that we can access it anywhere in our class.
class Greeter def call(env) @request = Rack::Request.new(env) case @request.path when "/" then Rack::Response.new(render("index.html.erb")) when "/change" then Rack::Response.new do |response| response.set_cookie("greet", @request.params["name"]) response.redirect("/") end else Rack::Response.new("Not found", 404) end end def render(template) path = File.expand_path("../views/#{template}", __FILE__) ERB.new(File.read(path)).result(binding) end def greet_name @request.cookies["greet"] || "World" end end
Now when we reload the page we’ll see the name we entered earlier.
Handling Multiple Requests
It’s important to be aware that only one Greeter
object is instantiated throughout the entire application. This means that if we were to set any other instance variables inside this class they could persist between requests which could potentially cause some tricky issues. We’ll change the Rackup file so that instead of instantiating a Greeter
here we pass in the class as a Rack application.
require "greeter" use Rack::Reloader, 0 run Greeter
This means that we can define call
as a class method in Greeter and instantiate a new greeter each time. This way there’s no chance that instance variables will persist between requests. We’ll also take the opportunity to do some renaming of the methods in the class here.
require "erb" class Greeter def self.call(env) new(env).response.finish end def initialize(env) @request = Rack::Request.new(env) end def response case @request.path when "/" then Rack::Response.new(render("index.html.erb")) when "/change" Rack::Response.new do |response| response.set_cookie("greet", @request.params["name"]) response.redirect("/") end else Rack::Response.new("Not Found", 404) end end def render(template) path = File.expand_path("../views/#{template}", __FILE__) ERB.new(File.read(path)).result(binding) end def greet_name @request.cookies["greet"] || "World" end end
We’ve done this so that we can easily call finish
on the Rack response
object that gets returned. This will convert it to the array format that Rack expects.
Another part of our application that could use some improvement is the HTML document. Currently its CSS is included in the HTML file but it would be better to use an external stylesheet with a link
tag. We’ll create the stylesheet in a new /public/stylesheets
directory and call it application.css
. We can paste the CSS from the HTML document into there and then reference it.
<head> <title>Greeter</title> <link rel="stylesheet" href="/stylesheets/application.css" type="text/css" charset="utt-8"> </head>
There’s a problem, though, in that our Rack application doesn’t know how to serve the static files inside the public directory. This solution might work in production where we have a separate server but we need something that will work in development.
There are a couple of ways that we can do this in Rack. One is to use a piece of Rack middleware called Rack::Static
. This is designed for serving static files but we haven’t had much luck getting it to work so instead we’ll use Rack::Cascade
. This isn’t middleware so we’re going to instantiate in the Rackup file.
require "greeter" use Rack::Reloader, 0 run Rack::Cascade.new([Rack::File.new("public"), Greeter])
We need to pass Rack::Cascade
an array of Rack applications. It will try the first Rack app in the array and if it gets a 404 response it will cascade to the next. The first app in our array is Rack::File
, which serves static files from the directory we pass it. Now when we request a file Rack::File
will look for that file and, if it’s not found, Rack::Cascade
will look for it in our Greeter
app.
The Rack::Reloader
middleware won’t pick up changes in the Rackup file so we’ll need to stop and restart the server. Our page still works when we reload it, even though we’re using an external stylesheet.
We can view the stylesheet through the Rack::File at http://localhost:9292/stylesheets/application.css
.
Adding Authentication
Let’s say that we want to protect our application with some simple authentication. We can use the Rack::Auth::Basic
middleware to do this.
require "greeter" use Rack::Reloader, 0 use Rack::Auth::Basic do |username, password| password == "secret" end run Rack::Cascade.new([Rack::File.new("public"), Greeter])
We pass Rack::Auth::Basic
a block and this takes username
and password
arguments. If the block returns true
then access is allowed. When we restart our Rack application and try visiting it again we’ll be asked for a username and password.
As long as we enter the correct password we’ll be able to access our application.
Adding Tests
There’s so much great middleware that we can’t cover it all here but we’ve given you some idea of the different options you can use in an Rack application. We’ll finish off this episode by adding some tests to our Rack application. Here’s our test code.
require "rubygems" require "rack" require "minitest/autorun" require File.expand_path("../lib/greeter", __FILE__) describe Greeter do before do @request = Rack::MockRequest.new(Greeter) end it "returns a 404 response for unknown requests" do @request.get("/unknown").status.must_equal 404 end it "/ displays Hello World by default" do @request.get("/").body.must_include "Hello World!" end it "/ displays the name passed into the cookie" do @request.get("/", "HTTP_COOKIE" => "greet=Ruby").body.must_include "Hello Ruby!" end it "/change sets cookie and redirects to root" do response = @request.post("/change", params: {"name" => "Ruby"}) response.status.must_equal 302 response["Location"].must_equal "/" response["Set-Cookie"].must_include "greet=Ruby" end end
We’re using Minitest here to get this up and running and Rack::MockRequest
to easily mock a request. We can call get
, post
, put
and delete
methods on this to make requests and methods like status
, body
, must_include
and so on on the response from those methods to test various parts of our application. We can run these tests by running the file through ruby
.
$ ruby greeter_test.rb Loaded suite greeter_test Started .... Finished in 0.332446 seconds. 4 tests, 9 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 441
There’s a lot more to Rack that we haven’t covered in this episode. There’s more information available in the documentation and you can browse through the classes listed here to see what each of them can do. If you want even more take a look at the Rack Contrib project. This includes a lot of useful utilities and middleware that you can use in your applications.