Dynamic WEBRick Servers in Ruby

Sometimes it's nice to have a self contained click-'n-run web-server - this way you are not dependent on presence of Apache, Mongrel, or any other 3rd party package. One way to go about this, is to do some socket programming. However, sockets may be too low-level for comfort, instead we can leverage yet another great Ruby library: WEBrick. Most Rails developers will be very familiar with WEBRick, but most don't realize how powerful it can be for quick mock-ups and one-off proofs of concept.

In my case, I wanted to run a quick, dynamic survey where the questions were pulled from the database, and the results would be persisted to disk. Specifically, I wanted to ask users to rank about a hundred different news stories for affect and impact. Thus, I needed to dynamically serve GET requests from the database, and also be able to process POST request (from the survey form) to persist my data. With the help of WEBRick I could do exactly this in about 60 lines of code.

Initializing WEBRick servers

A quick browse through the servlet examples, along with the RDoc gets us well on the way. We will start our server on port 8000, and mount two 'paths' - one for serving the survey, and second for processing the POST request (saving the form):

require 'rubygems'
require 'webrick'
require 'dbi'

# Initialize our WEBrick server
if $0 == __FILE__ then
  server = WEBrick::HTTPServer.new(:Port => 8000)
  server.mount "/questions", WebForm
  server.mount "/save", PersistAnswers
  trap "INT" do server.shutdown end
  server.start
end

Serving GET requests

From the initialization code, you will notice that we specified WebForm and PersistAnswers as the two classes that will process each request. Specifically, the user will first request to see the questions (issue a GET request), thus we need to define a WebForm class that will build and return to the user the HTML for our survey. To do this, we simply inherit from AbstractServlet, and define the do_GET method:

class WebForm < WEBrick::HTTPServlet::AbstractServlet

  # Process the request, return response
  def do_GET(request, response)
    status, content_type, body = print_questions(request)

    response.status = status
    response['Content-Type'] = content_type
    response.body = body
  end

  # Construct the return HTML page
  def print_questions(request)
    html   = "<html><body><form method='POST' action='/save'>"
    html += "Name: <input type='textbox' name='first_name' /><br /><br />";

    dbh = DBI.connect("DBI:Mysql:webarchive:localhost", "root", "pass")
    sth = dbh.execute("SELECT headline, story, id FROM yahoo_news where date >= '2004-12-01' and date <= '2005-01-01'")

      # iterate over every returned news-story from the database
      while row = sth.fetch_hash do
          html += "<b>#{row['headline']}</b><br />\n"
          html += "#{row['story']}<br />\n"
          html += "<input type='textbox' name='#{row['id']}' /><br /><br />\n"
     end
     sth.finish

    html += "<input type='submit'></form></body></html>"

    # Return OK (200), content-type: text/html, followed by the HTML itself
    return 200, "text/html", html
  end
end

Processing POST requests

In similar fashion, to process a POST request, we simply have to define the do_POST method:

class PersistAnswers < WEBrick::HTTPServlet::AbstractServlet

  def do_POST(request, response)
    status, content_type, body = save_answers(request)

    response.status = status
    response['Content-Type'] = content_type
    response.body = body
  end

  # Save POST request into a text file
  def save_answers(request)
    # Check if the user provided a name
    if (filename = request.query['first_name'] )
      f = File.open("save-#{filename}.#{Time.now.strftime('%H%M%S')}.txt", 'w')

      # Iterate over every POST'ed value and persist it to file
      request.query.collect { | key, value | f.write("#{key}: #{value}\n") }
      f.close
    end

    # Return OK (200), content-type: text/plain, and a plain-text "Saved! Thank you." notice
    return 200, "text/plain", "Saved! Thank you."
  end
end
webrick-servlet.rb - Combined final code

Believe it or not, that's it! You can extend this example to serve a file, an entire directory, or even run a full-blown Rails application. But with our basic setup, pointing your browser to localhost:8000 should produce:

[2007-02-13 15:15:21] INFO WEBrick 1.3.1
[2007-02-13 15:15:21] INFO ruby 1.8.5 (2006-08-25) [i386-mswin32]
[2007-02-13 15:15:21] INFO WEBrick::HTTPServer#start: pid=1224 port=8000
localhost - - [13/Feb/2007:15:15:31 PST] "GET /questions HTTP/1.1" 200 56679
- -> /questions
localhost - - [13/Feb/2007:15:15:36 PST] "POST /save HTTP/1.1" 200 17
http://localhost:8000/questions -> /save

Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.