LaunchSchool - An Online School for Developers /

Blog

Growing Your Own Web Framework With Rack Part 2

In part 1 of this series, we explained what Rack is and how to use it to build a simple web application. In this post, we’ll be expanding on that by adding in different URL endpoints. To do this we’ll also have to delve a bit deeper into how Rack keeps track of request data via headers.

Application Environment - env

We’ve been working with the application response within our Rack application. But, there is one other component of the call method that we have been ignoring, the env argument. What is the point of env, and why might we need it. Let’s take a closer look.

If we inspect the env, we might see the following.

  • GATEWAY_INTERFACE : CGI/1.1
  • PATH_INFO : /
  • QUERY_STRING :
  • REMOTE_ADDR : 127.0.0.1
  • REMOTE_HOST : 127.0.0.1
  • REQUEST_METHOD : GET
  • REQUEST_URI : http://localhost:9595/
  • SCRIPT_NAME :
  • SERVER_NAME : localhost
  • SERVER_PORT : 9595
  • SERVER_PROTOCOL : HTTP/1.1
  • SERVER_SOFTWARE : WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
  • HTTP_HOST : localhost:9595
  • HTTP_CONNECTION : keep-alive
  • HTTP_CACHE_CONTROL : max-age=0
  • HTTP_UPGRADE_INSECURE_REQUESTS : 1
  • HTTP_USER_AGENT : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like – Gecko) Chrome/53.0.2785.143 Safari/537.36
  • HTTP_ACCEPT : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
  • HTTP_ACCEPT_ENCODING : gzip, deflate, sdch
  • HTTP_ACCEPT_LANGUAGE : en-US,en;q=0.8
  • HTTP_COOKIE : _netflux_session=VTZoU1ZvVXlZV1MrQ3pTMHhlNXBSRGdpMFdXQXhrRFJnUGNMMEhhQmNLanp1aU9rb3pyQ3o2dGRE eVp0ZW5YTVJaSXBqZldIdWV1ZEtDRFdJWVo5b0FkMHRyZWVLVXVjR0lRdnV5dnl4VU01UWs0ZnBTbmlQc1Urb1g2ME1yU0pESkg0bGR2 bmwzR0h2bUt6a2xlQjlNNEpJNGtiOG1BQ2VkS0p5TWEvc1U0THdkNGtzbEtETmUrb1lDVHY5VWtKLS1oMnQ1cUlXcVJWcXZqTHpqWUNO L0JRPT0%3D—17bf5208879c831997c5c78c69895ded29aad26b
  • rack.version : [1, 3]
  • rack.input : #
  • rack.errors : #
  • rack.multithread : true
  • rack.multiprocess : false
  • rack.run_once : false
  • rack.url_scheme : http
  • rack.hijack? : true
  • rack.hijack : #
  • rack.hijack_io :
  • HTTP_VERSION : HTTP/1.1
  • REQUEST_PATH : /

This listing contains all the environment variables and information related to our HTTP request for the HelloWorld application. The env contains information regarding HTTP headers, as well as specific information about Rack.

Though it looks like an unorganized mess, this information is crucial for telling our server side code how to process the request. For example, looking at the REQUEST_PATH may tell which resource this request is retrieving and what query parameters are being attached with the request.

Let’s now get back to our application; we’ll revisit the env variable again later.

Routing: Adding in other pages to our application

We have an application that delivers a simple message, “Hello World!”, to the client no matter what type of request or query parameters are sent. But what if we want a bit more functionality in our application? We may want to send a dynamic string back to the client based on the request.

Let’s create another file to act as the storage for our dynamic content. We’ll pretend that our web application is going to send back some piece of advice, so we’ll create an Advice class in an advice.rb file. Our file structure now looks like this:

1
2
3
4
5
6
my_framework/
 ├── Gemfile
 ├── Gemfile.lock
 ├── config.ru
 ├── hello_world.rb
 └── advice.rb

Here’s the implementation for the Advice class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# advice.rb

class Advice
  def initialize
    @advice_list = [
      "Look deep into nature, and then you will understand everything better.",
      "I have found the paradox, that if you love until it hurts, there can be no more hurt, only more love.",
      "What we think, we become.",
      "Love all, trust a few, do wrong to none.",
      "Oh, my friend, it's not what they take away from you that counts. It's what you do with what you have left.",
      "Lost time is never found again.",
      "Nothing will work unless you do."
    ]
  end

  def generate
    @advice_list.sample
  end
end

The class above is not a separate web application like HelloWorld. Notice, that it doesn’t have a call method. This class will be solely used for content generation in our web application.

But, don’t we already have content in our HelloWorld app? In our current setup, no matter which URL we navigate to, we’ll always get the same response back, “Hello World!”. We don’t want to remove this functionality; our goal with the new Advice class is to augment our web app to now also dispense advice. Ideally, we want to be able to access both the “Hello World!” page as well as a page related to the getting some dynamic content from the Advice class.

This is where routing comes in. We’ll use some information given to us from the HTTP request to decide which URL to navigate to. To do this, we can use the environment variable, REQUEST_PATH. If the request path specifies '/' (the root path), then let’s show our usual “Hello World!” message. If the request path specifies '/advice', then we’ll reply with a random piece of advice. We’ll also add in one more route to handle any pages that don’t exist within our application so we’ll see a nice 404 message.

Let’s give it a try, and route to application pages, depending on the request path in the environment hash. Notice that the first line here loads the advice.rb file, which is where our Advice class lives. Also notice that line 9 below is where we’re using the Advice class to generate a random piece of advice, which is returned to the client.

Our first shot at introducing routing into our application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# hello_world.rb

require_relative 'advice'     # loads advice.rb

class HelloWorld
  def call(env)
    case env['REQUEST_PATH']
    when '/'
      ['200', {"Content-Type" => 'text/plain'}, ["Hello World!"]]
    when '/advice'
      piece_of_advice = Advice.new.generate    # random piece of advice
      ['200', {"Content-Type" => 'text/plain'}, [piece_of_advice]]
    else
      [
        '404',
        {"Content-Type" => 'text/plain', "Content-Length" => '13'},
        ["404 Not Found"]
      ]
    end
  end
end

Since we’ve made changes to our application, make sure you stop the server (Ctrl-C) and then restart it (bundle exec rackup config.ru -p 9595). Otherwise the new changes to our application won’t show up. Ok, let’s test all of this out and see how it looks.

Root Path (localhost:9595/)

Advice Path (localhost:9595/advice)

404 Not Found Path (localhost:9595/whatever)

Looks like our router is working and is returning the appropriate response based on the request path! But our application really lacks flexibility when it comes to presentation. With our current code, all we can do is return plain text. What if we wanted to improve the response a bit so the client (browser) can do a better job of displaying the response? For example, if we wanted to italicize some words, or add a header. To do that we’ll have to introduce HTML into the picture and expand our code base a bit.

Adding HTML to the Response Body

The client (browser) doesn’t do anything fancy with our response; all it does is it renders our HTTP response body as-is. Knowing that, let’s come up with a plan to spruce up the display:

  1. Make “Hello World!” an h2 header. We want this to be an emphatic greeting.
  2. Let’s italicize and bold our advice. We want the advice to be easy to read and clear.
  3. For the 404 page, we’ll make it an h4.

One other important change we’ll have to make below is to change the content type of our response. The HTML code listed below are really just string values. But setting the content type correctly will allow the client (browser) to know how to interpret the string and display it appropriately. If we don’t change it and leave it as "text/plain", then we’ll end up showing our HTML to the client, which isn’t what we want. We want the client to render that HTML code properly so that the text is displayed according to the HTML specification. Here’s an updated hello_world.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require_relative 'advice'

class HelloWorld
  def call(env)
    case env['REQUEST_PATH']
    when '/'
      [
        '200',
        {"Content-Type" => 'text/html'},
        ["<html><body><h2>Hello World!</h2></body></html>"]
      ]
    when '/advice'
      piece_of_advice = Advice.new.generate
      [
        '200',
        {"Content-Type" => 'text/html'},
        ["<html><body><b><em>#{piece_of_advice}</em></b></body></html>"]
      ]
    else
      [
        '404',
        {"Content-Type" => 'text/html', "Content-Length" => '48'},
        ["<html><body><h4>404 Not Found</h4></body></html>"]
      ]
    end
  end
end

We’ve made some significant changes to our response. The content type has been changed and the response body has been altered a bit to include HTML. Also note that on line 17, we had to use string interpolation to dynamically evaluate piece_of_advice into the HTTP response body.

Note that if we didn’t update the content type of our response then this is what we would see:

Root Path (localhost:9595/)

Advice Path (localhost:9595/advice)

404 Not Found Path (localhost:9595/whatever)

Seeing the HTML code in the browser like that is not what we want, and why we had to change the content type. That instructs the browser to parse the response for HTML tags and not just render it as-is.

As we did in the last section, let’s test our application and make sure it still works. Stop the server with Ctrl-C, then start up the server again:

1
$ bundle exec rackup config.ru -p 9595

Now let’s verify our changes.

Root Path (localhost:9595/)

Advice Path (localhost:9595/advice)

404 Not Found Path (localhost:9595/whatever)

Excellent. Everything still works as expected, and our new styling is being applied to each page as well. This is all good, but let’s take another look at our code. There are still some improvements that can be made. We’re writing the same code over and over again in those response bodies. HTML tags and body tags are listed in each of the three cases above. This is repetition that, if possible, we want to avoid.

One other issue is that our HTML responses are hardcoded in the routing code, and it feels very restrictive. As an application grows and changes, it may be necessary to return responses that are far more complex than what we currently have. Imagine trying to include all the HTML necessary for the front page of your favorite web application into the code we have. It would take up far too much space and make our call method unmanageable.

Conclusion

The main focus of this post has been on setting up a mechanism to return a conditional response based on the request. This is the start of a router. HTML was also added into the application. In part 3 of “Growing Your Own Framework With Rack”, we’ll continue to “grow” our app towards a framework by implementing a separation of concerns for our response generating code.

rack, ruby