Chapter 2. Fundamentals

In Chapter 1, we created Sinatra applications that had a single HTTP endpoint exposed as a route. In this chapter, we’ll explore the common HTTP verbs and their usage, how to construct more complex routes around those verbs, how to include static resources (such as raw HTML pages, stylesheets, images, and JavaScripts), and how to create dynamic HTML views. We’ll also cover working with HTTP headers, configuration blocks, and filters.

Note

This chapter will be the most reference-y of the book, with each topic covered using the briefest examples possible to avoid cluttering the discussion with unrelated facets. The next few chapters will delve into the more theoretical and architectural aspects of Sinatra, then we’ll turn our attention to practical application and create several projects that are more involved and cover a wider breadth of knowledge.

Routing

The core of any Sinatra application is the ability to respond to one or more routes. Routes are the primary way that users interact with your application. To understand how Sinatra handles routing, we must first examine HTTP, the Hypertext Transfer Protocol.

Note

The discussion takes place from the perspective of the server; a request is created by a client (which may be a browser, another web application, etc.), and a response is created by the server and sent back.

This is also true with regard to operating on request and response objects in Sinatra, which we will discuss throughout this chapter; the former will contain properties related to the client speaking to the server (such as location headers, cookie data, etc.), and the latter will contain information for the client to parse (such as content length, how long to cache something, and so on).

Hypertext Transfer Protocol

The HTTP specification is a network protocol that makes up the backbone of communication on the Internet between clients and servers.

When a client (which might be a web browser, a web application, a service, etc.) wants to interact with a web server over HTTP, it composes a message. An HTTP message is plain-text and line-oriented, making it very straightforward to construct and inspect. We saw this briefly in Chapter 1 when we composed requests to our application using Telnet.

Likewise, when a server is done processing a request, it can communicate information back to the client by creating its own HTTP message. The message usually contains information such as the status (did the client request succeed, was there an error in processing the request, etc.), what type of content is being sent back (plain text, an image, HTML, etc.) and other data which we’ll discuss throughout this chapter.

A message has the following characteristics, in order:

Start line

The start line is the first line in the request. It defines the HTTP verb to use, what resource to access, and denotes what version of HTTP is being used so that the server knows how to parse the request properly.

Headers

Headers provide additional information about the request. There are a number of standard headers that cover needs such as describing the length of the message, including the values of any cookies for that domain, and so on. It’s also possible to define custom headers that aren’t included in the HTTP specification.

It’s not required to have headers in an HTTP message. Any included headers have a name and a value, separated by a single colon. Each header is on its own line, and the headers section ends with a blank line.

Message body

The message body is the last item in the HTTP message, and it can contain any binary or text data. For example, when you upload an image to your favorite social network, the binary data for that image is stored in the message body and read by the server.

A message body is not required in an HTTP message.

Verbs

As mentioned, the start line of an HTTP message includes a verb. This defines the type of request being made and therefore how the server will interpret it; for instance, a GET will be treated very differently than a PUT (or at least it should be!).

For most development purposes, we can focus on five commonly-used verbs.

GET

A GET request is used to ask a server to return a representation of a resource. For example, when you browse to http://www.google.com/ the browser will issue a GET request; the server will (hopefully!) respond with the markup necessary for your browser to render the page markup. Additional resources (images, stylesheets, scripts, etc.) are requested by the browser as further GETs.

POST

A POST is used to submit data to a web server. Arguably the most ubiquitous example of a POST is the humble login form. A user fills in his credentials, clicks a button, and the browser submits the data via a POST to the server. The server can then respond accordingly based on the payload.

PUT

PUT is used to create or update a representation of a resource on a server. If you were in the process of adding photos to an online album, you could create PUT requests that contain the complete contents of the resource, which would then be available at a unique URL on the server.

Note

The line between POST and PUT blurs slightly in practice; the real difference between the two verbs lies in how the server should handle the payload. If the request is a POST, the current URL may handle payload application, but if the request is a PUT the supplied location must be what handles it.

In simpler terms, you might POST some data to a form that is designed to accept a variety of input and apply it to one or more resources in your application. Your POST indicates what location on the server will handle the process, but doesn’t necessarily map to any one particular resource. A PUT request, by contrast, should refer to one (and only one) resource in particular.

If it’s still unclear, you can find a more in-depth discussion at http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html.

DELETE

DELETE is used to destroy a resource on a server.

Note

In practice, although PUT and DELETE have unique identities, their functionality is often expressed via POSTs in web applications. The reason is fairly mundane: the HTML <form> element supports only GET and POST as available actions.

Some frameworks circumvent this by providing a hidden <input> field whose value represents the verb to use, and you can certainly use the other verbs via JavaScript and client libraries.

Warning

GET, PUT, and DELETE are expected to exhibit what is termed idempotence: an action that is idempotent should deliver identical results if the action is repeated. POST is not considered idempotent as repeated POST requests may continually update the server and return different results.

PATCH

PATCH is used to update a portion of a resource; this is in contrast to PUT, which replaces it wholesale.

Warning

PATCH is not required to be idempotent, as it is conceivable that partial resource updates may require a known starting point or risk corruption.

RFC 5789 contains additional helpful information on the semantics of the PATCH verb; you can investigate further at http://tools.ietf.org/html/rfc5789.

Warning

PATCH is new to Sinatra as of version 1.3.0; if you try to create a route that responds to it in earlier versions, you will be rewarded with an undefined method exception.

Note

The HTTP specification defines several verbs we won’t be specifically discussing here: OPTIONS, HEAD, TRACE, and CONNECT (of which the last two are not natively supported by Sinatra). These fulfill various functions, such as returning headers, enumerating options on the server, and so on.

Collectively, these verbs make up the vocabulary that Sinatra uses to express the definition of a route. There are a variety of ways to configure routes in Sinatra; we’ll examine both the common and the more esoteric. Regardless of their individual semantics, all route forms revolve around the HTTP verbs and follow a general pattern.

Common Route Definition

We saw the basic type of route definition in the sample application in Chapter 1. To declare a route in Sinatra, you must supply the HTTP verb to respond to, the specific URL, and then optionally define the behavior desired for the route. See Example 2-1 for the common form of route definition.

Example 2-1. The common form of route definition
require 'sinatra'

get '/' do
  "Triggered via GET"
end

post '/' do
  "Triggered via POST"
end

put '/' do
  "Triggered via PUT"
end

delete '/' do
  "Triggered via DELETE"
end

patch '/' do
  "Triggered via PATCH"
end

options '/' do
  "Triggered via OPTIONS"
end

Note

There are a number of extensions to Sinatra available in the sinatra-contrib project on GitHub. One of them, Sinatra::MultiRoute, allows for the creation of routes with verbs not normally supported by Sinatra (among other things).

The project can be found at https://github.com/sinatra/sinatra-contrib.

Many URLs, Similar Behaviors

Sometimes you may encounter situations where multiple routes should respond the same way. We’re always trying to keep our code DRY, and luckily there is an approach that comes in handy without being unwieldy; Example 2-2 demonstrates how to respond to an array of routes by verb.

Note

DRY stands for “Don’t Repeat Yourself”; it’s also occasionally expressed as DIE, or “Duplication is Evil.”

Example 2-2. Many URLs sharing a handler
require 'sinatra'

['/one', '/two', '/three'].each do |route|
  get route do
	  "Triggered #{route} via GET"
  end

  post route do
    "Triggered #{route} via POST"
  end

  put route do
    "Triggered #{route} via PUT"
  end

  delete route do
    "Triggered #{route} via DELETE"
  end

  patch route do
    "Triggered #{route} via PATCH"
  end
end

Note

Notice that the URLs in Example 2-2 do not have trailing slashes. In fact, if you try to browse to http://localhost:4567/one/, it won’t work.

If you’d like to make the trailing slash optional, simply add a slash and a question mark to the end of the URL.

get('/one/?') { ... }

Routes with Parameters

Routes in Sinatra can also accept parameters that are exposed in code via the params array, as shown in Example 2-3.

Example 2-3. Accessing parameters in the request
require 'sinatra'
						
get '/:name' do
  "Hello, #{params[:name]}!"
end

In the case of data submission requests, the data in the payload is available in the params array. Unlike GET and DELETE requests, you don’t need to specify the payload parameters in the URL. Example 2-4 shows this in practice.

Example 2-4. Data payloads are stored in the usual array.
require 'sinatra'
						
post '/login' do
  username = params[:username]
  password = params[:password]
end

put '/users/:id' do
  # let's assume we could retrieve a User
  u = User.find(params[:id])
  u.first_name = params[:first_name]
  u.last_name = params[:last_name]
  u.save
end

Routes with Query String Parameters

In addition to parameters that are used to compose the URL itself, Sinatra also stores query string parameters by name in the params array. See Example 2-5.

Example 2-5. Retrieving query string parameters
require 'sinatra'
						
get '/:name' do
  # assumes a URL in the form /some_name?foo=XYZ
  "You asked for #{params[:name]} as well as #{params[:foo]}"
end

Routes with Wildcards

Routes in Sinatra can also accept wildcards in the form of the splat (*) character, as demonstrated in Example 2-6. Anything passed in the wildcard position is stored in params[:splat], which itself contains an array.

Example 2-6. Using wildcards in a route
require 'sinatra'

get '/*' do
  "You passed in #{params[:splat]}"
end

The route described in Example 2-6 is a greedy match; for example, if you run the code and browse to http://localhost:4567/foo/bar, the output on the screen will be You passed in ["foo/bar"]. If you browse to http://localhost:4567/foo/bar/baz/bop, the output will be You passed in ["foo/bar/baz/bop"].

This brings us to a very important Sinatra routing tenet. It’s a simple rule, but critical to emphasize, especially as we move into static files and views.

The First Sufficient Match Wins

When Sinatra parses routes, the first sufficient match is the one that will be executed. This is true even when there is a better or more specific route definition later in the file. Let’s take a look at a route configuration in Example 2-7, where a greedy match eats up a more specific one.

Example 2-7. Demonstrating Sinatra’s “first sufficient match” approach
require 'sinatra'
					
get '/*' do
  "NOM NOM NOM"
end

get '/specific' do
  "You'll never, ever see me."
end

Browsing to http://localhost:4567/specific should return NOM NOM NOM even though there is a better match later in the file. It is an easily avoidable problem, but very important to bear in mind when working with a complex set of route definitions.

Note

Sinatra also allows us to include static resources in our applications, such as CSS files, JavaScripts, images, and HTML files.

Pop quiz time! Let’s say we have a static file called public.html that contains the text This is a static file, and we also define a route in the form get('/public.html') { "This is delivered via the route." }.

Given that we have two definitions for the same resource, what will be displayed when browsing to http://localhost:4567/public.html? We’ll discuss the answer in just a moment.

Routes with Regular Expressions

Sinatra also accepts regular expressions as a way to match incoming requests to particular handlers. Because of their flexible nature, we’ll also use Example 2-8 to reinforce the dangers of greedy matches.

Example 2-8. Careless regular expressions can lead to greedy bugs
require 'sinatra'

get %r{/(sp|gr)eedy} do
  "You got caught in the greedy route!"
end

get '/speedy' do
  "No one calls me :("
end

get '/greedy' do
  "No one calls me either!"
end

As we’re sure you can guess, the regular expression match is the first sufficient match in comparison to the routes defined later, and therefore the later routes will not be executed. This isn’t to say that using regular expressions to match routes is a bad idea. We just mean to convey that some caution should be exercised (as well as for wildcard matches).

Halting a Request

Sometimes we don’t want an operation to continue; maybe a critical error has occurred, or perhaps a process is taking too long and we’d like to bail out. Sinatra provides a halt method for just this purpose as shown in Example 2-9.

Example 2-9. Using halt to stop a request
require 'sinatra'

get '/halt' do
  'You will not see this output.'
  halt 500
end

Now let’s use cURL to see what happens when this route is executed:

$ curl -v http://localhost:4567/halt
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /halt HTTP/1.1
> User-Agent: curl/7.21.2 (x86_64-apple-darwin10.4.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< Connection: close
< Date: Sat, 17 Sep 2011 21:09:42 GMT
< Content-Type: text/html;charset=utf-8
< Content-Length: 0
< 
* Closing connection #0

A status code of 500 Internal Server Error was returned, and the text we entered was not delivered to the client (further verified by a Content-Length header of value 0).

Passing a Request

Depending on the structure of your application, there may be instances where you’d like to pass processing on to the next best matching route (if available). To do so, simply define the criteria on which to match, and use the pass method to look for the next available match. See Example 2-10.

Example 2-10. Passing to another matching route
require 'sinatra'

before do
  content_type :txt
end

get %r{/(sp|gr)eedy} do
  pass if request.path =~ //speedy/
  "You got caught in the greedy route!"
end

get '/speedy' do
  "You must have passed to me!"
end

Now, if a user requests /speedy, she will actually reach the regular expression-based route handler first, then be passed along to the latter handler:

$ curl http://localhost:4567/greedy
  You got caught in the greedy route!

$ curl http://localhost:4567/speedy
  You must have passed to me!

Redirecting a Request

You can redirect a request to a different location using the redirect method. Optionally, you can provide a status code as shown in Example 2-11(to differentiate between a temporary and permanent redirection, for example).

Example 2-11. Redirect a request with optional status codes
require 'sinatra'

get '/redirect' do
  redirect 'http://www.google.com'
end

get '/redirect2' do
  redirect 'http://www.google.com', 301
end

Let’s examine these routes via cURL again to see the difference that the status codes make:

$ curl -v http://localhost:4567/redirect
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /redirect HTTP/1.1
> User-Agent: curl/7.21.2 (x86_64-apple-darwin10.4.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 302 Moved Temporarily
< Connection: close
< Date: Sat, 17 Sep 2011 21:27:45 GMT
< Content-Type: text/html;charset=utf-8
< Location: http://www.google.com
< Content-Length: 0
< 
* Closing connection #0

$ curl -v http://localhost:4567/redirect2
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /redirect2 HTTP/1.1
> User-Agent: curl/7.21.2 (x86_64-apple-darwin10.4.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Connection: close
< Date: Sat, 17 Sep 2011 21:27:47 GMT
< Content-Type: text/html;charset=utf-8
< Location: http://www.google.com
< Content-Length: 0
< 
* Closing connection #0

By default, if you do not provide a status code to the redirect method, it will assume you want a temporary redirection (code 302). This indicates to clients that they would normally find the resource they were looking for at the requested URL, but for the moment it is available elsewhere. A 301 redirection tells the client that the URL they requested may have been correct in the past, but the current location is elsewhere (and in the future, the client should only request that new location).

Static Files

As Sinatra developers, we’re not bound to route creation as a way to deliver static content. In Examples 2-12 and 2-13, assume that we have a subfolder named public that contains a single file, public.html.

Example 2-12. A simple HTML file
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Static file</title>
  </head>
  <body>
    <h1>This is a static file.</h1>
  </body>
</html>
Example 2-13. A Sinatra application with a route conflict
require 'sinatra'
				
get '/public.html' do
  'This is delivered via the route.'
end

Earlier we posed the question of what would be delivered to the client in the event that a defined route conflicted with the name of a static resource. The answer to that question is shown in Figure 2-1.

The static resource is delivered instead of the route content
Figure 2-1. The static resource is delivered instead of the route content

You’ll notice that the public folder is omitted from the URL. In the case of static files, views, and so on, the folder is assumed and not included in any public-facing component. For example, a folder called javascripts inside the public folder would be accessible through http://localhost:4567/javascripts.

Note

If you’d like to use a different location than public for your static resources, you’re free to do so (although most applications simply use the default convention). You can swap the location with set :public_folder, File.dirname(__FILE__) + '/your_custom_location'.

This and several other configuration settings that we will call out in this chapter should be placed in a special block known as the configure block. We’ll discuss how to use it shortly.

Warning

In older versions of Sinatra, the symbol was :public rather than :public_folder; this has changed in 1.3.0 to help avoid collisions with Ruby’s built-in public keyword. The :public symbol will still work, but it will offer up a warning: :public is no longer used to avoid overloading Module#public.

Views

Views in Sinatra are HTML templates that can optionally contain data passed from the application. There are numerous template engines available; Erb, Haml, and Erubis are just a few. We’ll use Erb for our templates as it’s fairly ubiquitous in the Ruby community these days, although any mature engine will do just fine as the usage is essentially the same in all cases.

Note

Erb is also the template engine of choice for Rails, and David Heinemeier Hansson (author of the Rails framework) has indicated that this is unlikely to change any time soon.

There are two ways to interact with views in Sinatra: inline templates and external templates.

Inline Templates

Inline templates, unsurprisingly, are defined in the application code file itself. They are located at the bottom of the file; Example 2-14 shows how to include templates directly in your application files.

Note

You can also have inline templates defined in other files, although by default only the ones in the application file get automatically loaded. You’ll need to call enable :inline_templates in a configure block to bring in the others.

Example 2-14. Defining an Erb template using the inline approach
require 'sinatra'

get '/index' do
  erb :index
end

__END__

@@index 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Inline template</title>
  </head>
  <body>
    <h1>Worked!</h1>
  </body>
</html>

It’s important to note that the name of the view must be a symbol. Inline templates require that you create a class variable of the same name so that Sinatra knows which template to render.

External View Files

Things can get pretty cluttered if you’re relying solely on inline templates for your view needs. If you’d prefer to store your views externally, Sinatra will look for them by default in the views subfolder. In Example 2-15, a symbol containing the filename (up to the extension) is passed as the parameter for rendering. Example 2-16 shows a simple Erb file that will be rendered by the route.

Note

As with static resources, you can change the location of your views: set :views, File.dirname(__FILE__) + '/your_custom_location'. This is defined in the configure block.

Example 2-15. The Erb template has been extracted
require 'sinatra'

get '/index' do
  erb :index
end
Example 2-16. The contents of index.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>External template</title>
  </head>
  <body>
    <h1>Worked!</h1>
  </body>
</html>

External Views in Subfolders

In the spirit of reducing clutter, we may not always want all our external templates plopped into one folder. To reference views in subfolders, we need to convert a string representation of the path to a symbol. There are two ways to do this, both of which are demonstrated in Example 2-17.

Example 2-17. Referencing a view in a subfolder
require 'sinatra'
						
get '/:user/profile' do
  erb '/user/profile'.to_sym
end

get '/:user/help' do
  erb :'/user/help'
end

This code will look for files called profile.erb and help.erb in /views/user. Either approach for creating the symbol is acceptable. Anecdotally, the use of #to_sym seems to be more common in practice.

Note

If you want to get technical, one could also use #intern as well.

Passing Data into Views

Information constructed in the back-end of the application can be shared to the front-end view through the use of instance variables, as shown in Examples 2-18 and 2-19.

Example 2-18. Creating instance variables for use in a view
require 'sinatra'
					
get '/home' do
  @name = 'Random User'
  erb :home
end
Example 2-19. Accessing instance variables in a view
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Using instance variables</title>
</head>
<body>
  <h1>Hello, <%= @name %>!</h1>
</body>
</html>

Statements to be evaluated (displaying instance variables, performing transformation operations, etc.) are wrapped in <%= %> tags, while control statements (loops, etc.) are wrapped in <% %> tags. Example 2-20 shows how to create instance variables for use in a view, while Example 2-21 demonstrates the consumption of those variables (as well as the difference between evaluated and control statements in Erb).

Example 2-20. Iterating over a loop in a view
require 'sinatra'
				
get '/home' do
  @users = ['Sally', 'Jerry', 'Rocko']
  erb :home
end
Example 2-21. Accessing instance variables in a view using a loop
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Using instance variables</title>
</head>
<body>
  <% @users.each do |user| %>
    <p><%= user %></p>
  <% end %>
</body>
</html>

Filters

Sinatra supports filters as a way to modify requests and responses, both before and after a route has been executed; Example 2-22 uses a before filter to set an instance variable prior to processing a route.

Example 2-22. Using before and after filters
require 'sinatra'
				
before do
  @before_value = 'foo'
end

get '/' do
  "before_value has been set to #{@before_value}"
end

after do
  puts "After filter called to perform some task."
end

The after filter output is shown in Figure 2-2, where After filter called to perform some task. has been displayed in the output stream.

The static resource is delivered instead of the route content
Figure 2-2. The static resource is delivered instead of the route content

Note

The before and after filters are identical in form to the route methods; specifically, you can provide a URL to a filter and Sinatra will match it.

before('/index') { ... } # executed only before the '/index' route

If you do not supply a URL (as shown in Example 2-22) the block will be executed before or after every request respectively.

Handling Errors

Anyone who has spent a significant amount of time writing code (especially for the web) can tell you that despite our best intentions, sometimes things go wrong. Two common problems in web applications are in the 400 and 500 error ranges.

The HTTP specification defines a number of ranges that indicate the type of response that a server is sending to a client. For example, the 200–299 range is reserved for responses that indicate success in processing a request; this may involve creating a resource, returning markup, or similar tasks. By contrast, the 500–599 range is reserved for server errors, where processing was interrupted because of some unrecoverable condition on the application’s side of the conversation (assuming that the client delivered a valid request).

Sinatra wraps up two common problems with helpers: 404 (Not Found) and 500 (Internal Server Error).

Note

These helpers are in the same context as normal routes and before filters, and can therefore enjoy the same conveniences; this includes rendering engine shortcuts (such as erb), the request and response objects, and so on.

404 Not Found

To easily handle the absence of a route or resource matching a particular request, you can render some form of output via the not_found block, as shown in Example 2-23.

Example 2-23. Gracefully handling 404 errors
require 'sinatra'

	before do
	  content_type :txt
	end

	not_found do
	  "Whoops! You requested a route that wasn't available."
	end

Now, since there are no conventional routes defined, any requests will be handled by the not_found call.

$ curl -v http://localhost:4567/foo
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /foo HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< X-Frame-Options: sameorigin
< X-XSS-Protection: 1; mode=block
< Content-Type: text/html;charset=utf-8
< X-Cascade: pass
< Content-Length: 52
< Connection: keep-alive
< Server: thin 1.2.11 codename Bat-Shit Crazy
< 
* Connection #0 to host localhost left intact
* Closing connection #0
Whoops! You requested a route that wasn't available.

500 Internal Server Error

In the event that an unhandled exception occurs while processing a request, you can handle it by defining an error block, as shown in Example 2-24.

Example 2-24. Gracefully handling 500 errors
require 'sinatra'

	before do
	  content_type :txt
	end

	configure do
	  set :show_exceptions, false
	end

	get '/div_by_zero' do
	  0 / 0
	  "You won't see me."
	end

	error do
	  "Y U NO WORK?"
	end

The /div_by_zero has an obvious unhandled exception in the division by zero operation. When encountered, this will result in the output of the error block being returned to the client.

Warning

A logical question to ask is, What happens if an unhandled exception occurs in the error-handling block? The answer is that nothing will be returned to the client; the response will be totally empty (no headers, no body, etc.).

$ curl -v http://localhost:4567/div_by_zero
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /div_by_zero HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< X-Frame-Options: sameorigin
< X-XSS-Protection: 1; mode=block
< Content-Type: text/plain;charset=utf-8
< Content-Length: 12
< Connection: keep-alive
< Server: thin 1.2.11 codename Bat-Shit Crazy
< 
* Connection #0 to host localhost left intact
* Closing connection #0
Y U NO WORK?

Configuration

For your configuration needs, Sinatra makes available a method called configure that can be used globally or for specific routes or environments.

In the code shown in Example 2-25, the configure block is used to register a new MIME type in the form of a symbol, :plain. Next, a before block sets the content type that will be used to render a response for the route http://localhost:4567/plain-text. While the http://localhost:4567/html route will return HTML content, the http://localhost:4567/plain-text route will return only text, which the browser will not render or style.

Note

MIME is short for Multipurpose Internet Mail Extensions. MIME types are standard ways of indicating to clients how to interpret or handle a particular piece of content. They were initially developed with regard to email clients, but have since expanded into other application types (such as web browsers).

Example 2-25. Using a configure block
require 'sinatra'

configure do
  mime_type :plain, 'text/plain'
end

before '/plain-text' do
  content_type :plain
end

get '/html' do
  '<h1>You should see HTML rendered.</h1>'
end

get '/plain-text' do
  '<h1>You should see plain text rendered.</h1>'
end

HTTP Headers

HTTP requests and responses are associated with a number of headers that provide additional information to clients and servers. For example, there are headers that are used to denote how long something should be cached, how long the content is, and so on. It’s also possible to create custom headers of your own.

The headers Method

Sinatra provides a method called headers that can be used to set HTTP headers in your responses, which is demonstrated in Example 2-26. It accepts a hash where the key is the name of the header.

Example 2-26. Setting custom HTTP headers
require 'sinatra'

before do
  content_type :txt
end

get '/' do
  headers "X-Custom-Value" => "This is a custom HTTP header."
  'Custom header set'
end

get '/multiple' do
  headers "X-Custom-Value" => "foo", "X-Custom-Value-2" => "bar"
  'Multiple custom headers set'
end

You can check these headers with a quick run via cURL.

$ curl -v localhost:4567/
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain;charset=utf-8
< X-Custom-Value: This is a custom HTTP header.
< Content-Length: 17
< Connection: keep-alive
< Server: thin 1.2.11 codename Bat-Shit Crazy
< 
* Connection #0 to host localhost left intact
* Closing connection #0

$ curl -v localhost:4567/multiple
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /multiple HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain;charset=utf-8
< X-Custom-Value: foo
< X-Custom-Value-2: bar
< Content-Length: 27
< Connection: keep-alive
< Server: thin 1.2.11 codename Bat-Shit Crazy
< 
* Connection #0 to host localhost left intact
* Closing connection #0

Note

The HTTP specification defines a number of standard headers; the generally-accepted convention is to prefix custom or user-defined headers with X-, such as X-Custom-Value. There are several non-standard headers that are in common use: X-Forwarded-For, X-Requested-With, and X-Powered-By are just a few.

The standard HTTP headers can be found at http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html.

Exploring the request Object

Sinatra exposes a variety of request information via the request object, as seen in Example 2-27. The request object holds a hash of request data such as who made the request, what version of the HTTP standard to use, and so on. We’ll discuss the underlying mechanics of this object further when we delve into Rack in Chapter 3.

Example 2-27. Accessing request data
require 'sinatra'

before do
  content_type :txt
end

get '/' do
  request.env.map { |e| e.to_s + "
" }
end

This application will iterate over all the values in the @env variable and display them as output.

$ curl http://localhost:9393/
["GATEWAY_INTERFACE", "CGI/1.1"]
["PATH_INFO", "/"]
["QUERY_STRING", ""]
["REMOTE_ADDR", "127.0.0.1"]
["REMOTE_HOST", "localhost"]
["REQUEST_METHOD", "GET"]
["REQUEST_URI", "http://localhost:9393/"]
["SCRIPT_NAME", ""]
["SERVER_NAME", "localhost"]
["SERVER_PORT", "9393"]
["SERVER_PROTOCOL", "HTTP/1.1"]
["SERVER_SOFTWARE", "WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)"]
["HTTP_USER_AGENT", "curl/7.19.7 (universal-apple-darwin10.0)"]
["HTTP_HOST", "localhost:9393"]
["HTTP_ACCEPT", "*/*"]
["rack.version", [1, 1]]
["rack.input", #<Rack::Lint::InputWrapper:0x0000010327f368 @input=#<StringIO:0x000001009c8500>>]
["rack.errors", #<Rack::Lint::ErrorWrapper:0x0000010327f2f0 @error=#<IO:<STDERR>>>]
["rack.multithread", true]
["rack.multiprocess", false]
["rack.run_once", false]
["rack.url_scheme", "http"]
["HTTP_VERSION", "HTTP/1.1"]
["REQUEST_PATH", "/"]
["rack.request.query_string", ""]
["rack.request.query_hash", {}]

Similarly, we can iterate over the methods defined for the request object for comparison purposes. See Example 2-28.

Example 2-28. Enumerating the methods on the request object
require 'sinatra'

before do
  content_type :txt
end

get '/' do
  request.methods.map { |m| m.to_s + "
" }
end

We’ve truncated the rendered output for brevity, but you’ll see a number of the @env fields represented.

$ curl localhost:4567/
accept
preferred_type
accept?
secure?
forwarded?
safe?
idempotent?
env
body
script_name
path_info
request_method
query_string
content_length
content_type
session
session_options
logger
media_type
media_type_params
content_charset
scheme
ssl?
referer
referrer
user_agent
cookies
xhr?
base_url
url
path
fullpath
accept_encoding
ip
parse_query
parse_multipart

Caching

Caching in web applications can be a complex topic, spanning a variety of tiers. Browsers, proxies, servers, and other devices can all cache content and resources independently. There are a number of HTTP headers that can be used to inform consumers of how to handle content; we’ll examine a few here.

Note

For the purposes of this conversation, we can assume that caching refers to control provided by HTTP headers and is not inclusive of external services such as Memcached.

Setting Headers Manually

Using the headers helper we discussed earlier, you can set any desired headers to influence downstream caching. The code in Example 2-29 will inform consumers that the content should be cached for one hour.

Example 2-29. Setting cache headers manually
require 'sinatra'

before do
  content_type :txt
end

get '/' do
  headers "Cache-Control" => "public, must-revalidate, max-age=3600",
          "Expires" => Time.at(Time.now.to_i + (60 * 60)).to_s
  "This page rendered at #{Time.now}."
end

If you visit this page in a browser, refreshing will not update the time displayed on the page. The headers are shown in the cURL session below.

$ curl -v http://localhost:9393/
* About to connect() to localhost port 9393 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 9393 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:9393
> Accept: */*
>
< HTTP/1.1 200 OK 
< Content-Type: text/plain;charset=utf-8
< Cache-Control: public, must-revalidate, max-age=3600
< Expires: 2011-09-26 17:35:04 -0400
< Content-Length: 48
< Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
< Date: Mon, 26 Sep 2011 20:35:04 GMT
< Connection: Keep-Alive
< 
* Connection #0 to host localhost left intact
* Closing connection #0
This page rendered at 2011-09-26 16:35:04 -0400.

Settings Headers via expires

Sinatra has a convenient shortcut for defining cache control and content expiration that wraps up the behavior described above in a shorter method call. Specifically, you can set the appropriate headers using the expires helper as shown in Example 2-30.

You can use the cache_control helper if you only want to set the Cache-Control header.

Example 2-30. Setting content expiration using expires
require 'sinatra'

before do
  content_type :txt
end

get '/cache' do
  expires 3600, :public, :must_revalidate
  "This page rendered at #{Time.now}."
end

This will produce the same effect as Example 2-29.

Note

A good resource for understanding the various caching options available via HTTP can be found at http://www.mnot.net/cache_docs/.

ETags

ETags, short for entity tags, are another way to represent how fresh a resource is via HTTP. They are server-generated identifiers that are used to fingerprint a resource in a given state. A client can safely assume that if the ETag for a resource has changed, then the resource itself has changed and should be fetched from the server again.

Note

There’s no special significance to the ETag value itself; it could be a globally-unique identifier (GUID), a checksum, etc. There is no meaning attached to the particular value.

Generating ETags

ETags are nothing more than HTTP headers with values crafted by the server for consumption by clients. As such, you could use the headers helper to set them (as demonstrated by previous examples). There is a built-in helper specifically for ETags called, conveniently enough, etag. Example 2-31 demonstrates generating an ETag.

Example 2-31. Generating an ETag
require 'sinatra'
require 'uuid'

before do
  content_type :txt
  @guid = UUID.new.generate
end

get '/etag' do
  etag @guid
  "This resource has an ETag value of #{@guid}."
end

A request to this resource is now associated with a specific ETag value, which can be compared on demand to the value returned by the server to determine if anything has changed.

$ curl -v http://localhost:9393/etag
* About to connect() to localhost port 9393 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 9393 (#0)
> GET /etag HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:9393
> Accept: */*
> 
< HTTP/1.1 200 OK 
< Content-Type: text/plain;charset=utf-8
< Etag: "448c1ee0-cab2-012e-0f5e-482a14372ddf"
< Content-Length: 72
< Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
< Date: Mon, 26 Sep 2011 21:13:13 GMT
< Connection: Keep-Alive
< 
* Connection #0 to host localhost left intact
* Closing connection #0
This resource has an ETag value of 448c1ee0-cab2-012e-0f5e-482a14372ddf.

Weak ETags

Normal ETags are considered to be strongly-validating; two identical ETags can be considered to refer to byte-for-byte identical resources. There is an alternative, the weak ETag, that is weakly-validating. Weak ETags are used to denote that resources can be considered identical or equivalent even if they are not byte-for-byte identical.

Weak ETags can be generated by passing the symbol :weak to the etag helper, as shown in Example 2-32.

Example 2-32. Generating a weak ETag
require 'sinatra'
require 'uuid'

before do
  content_type :txt
  @guid = UUID.new.generate
end

get '/etag' do
  etag @guid, :weak
  "This resource has an ETag value of #{@guid}."
end

Examining the response headers, we can see that the ETag value is prepended by W/ to denote that the ETag is weak.

$ curl -v http://localhost:9393/etag
* About to connect() to localhost port 9393 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 9393 (#0)
> GET /etag HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:9393
> Accept: */*
> 
< HTTP/1.1 200 OK 
< Content-Type: text/plain;charset=utf-8
< Etag: W/"448c1ee0-cab2-012e-0f5e-482a14372ddf"
< Content-Length: 72
< Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
< Date: Mon, 26 Sep 2011 21:13:13 GMT
< Connection: Keep-Alive
< 
* Connection #0 to host localhost left intact
* Closing connection #0
This resource has an ETag value of 448c1ee0-cab2-012e-0f5e-482a14372ddf.

Note

The server will send the ETag header, but the client will send If-None-Match to the server to validate the ETag. If you want to check ETag values from the client in your application code, look for If-None-Match.

Sessions

Although HTTP is itself a stateless protocol, one way to maintain the state for a user is through the use of cookie-based sessions, demonstrated in Example 2-33. In this approach, a cookie (in this case, one named rack.session) is stored client-side and used to house data related to the activity in the current user’s session.

Note

A more thorough discussion of cookies in general is up next.

You can enable sessions via the configure block. Once enabled, the session object can be used to store and retrieve values.

Example 2-33. Using cookie-based session management
require 'sinatra'

configure do
  enable :sessions
end

before do
  content_type :txt
end

get '/set' do
  session[:foo] = Time.now
  "Session value set."
end

get '/fetch' do
  "Session value: #{session[:foo]}"
end

It wouldn’t be very secure if we could just decode any Sinatra session out there. For the sake of security, Sinatra creates a secret key for you each time the application is started and uses this to protect the session state data.

There are a number of reasons you might want to set this key manually; for example, you may want to operate multiple application instances behind a load-balancer and therefore need to be able to decode session state properly on discrete servers. To set the secret key, you can do so in a configure block via set :session_secret, 'your_custom_value_here'.

Warning

One caveat with cookie-based sessions: any data you store in the session collection is serialized and stored on the client. It’s important to be mindful of network traffic and performance with this approach, especially if you’re used to other session storage approaches where only a unique key is stored client-side (with the actual session data stored in memory on the server).

Destroying a Session

If you’d like to destroy a session, you can call session.clear in your routes to immediately wipe out that user’s session. See Example 2-34.

Example 2-34. Destroying a session using session.clear
require 'sinatra'

configure do
  enable :sessions
end

before do
  content_type :txt
end

get '/set' do
  session[:foo] = Time.now
  "Session value set."
end

get '/fetch' do
  "Session value: #{session[:foo]}"
end

get '/logout' do
  session.clear
  redirect '/fetch'
end

Cookies

Cookies are small amounts of metadata stored client-side. There are essentially two types of cookies: session and persistent. They differ by the points at which they expire; session cookies are destroyed when the user closes his browser (or otherwise ends his session), and persistent cookies expire at a predetermined time that is stored with the cookie itself.

Although it’s easy to think of cookies as being set server-side, the server is actually simply asking the client to take the contents of the Set-Cookie header from the response and persist it for some length of time. The client can, at its discretion, opt to send the data back to the server on subsequent requests.

Setting a cookie, in simplest form, is accomplished by calling response.set_cookie and providing a name and value as parameters; deleting is accomplished in similar fashion using repsonse.delete_cookie. Reading a cookie involves accessing the request.cookies collection by name. See Example 2-35 for more on working with cookies.

Example 2-35. Working with cookies
require 'sinatra'

get '/' do
  response.set_cookie "foo", "bar"
  "Cookie set. Would you like to <a href='/read'>read it</a>?"
end

get '/read' do
  "Cookie has a value of: #{request.cookies['foo']}."
end

get '/delete' do
  response.delete_cookie "foo"
  "Cookie has been deleted."
end

Warning

There are a number of optional settings for cookies, including the domain and path that the cookie can be set for. By default, clients typically set cookie paths relative to the requesting URL if none is explicitly provided. If, for example, your route were:

get('/set') { response.set_cookie "foo", "bar" }

then the cookie foo would not be sent to the server on requests to:

get('/read') { ... }

To set any of the additional options explicitly, you can pass a hash containing your settings as the second parameter:

response.set_cookie("foo", :value => "bar", :path => '/')

Attachments

As demonstrated in Example 2-36, sending attachments to clients is extremely easy in Sinatra; there is a built-in attachment method that optionally takes a filename parameter. If the filename has an extension (.jpg, .txt, etc.), that extension will be used to determine the Content-Type header for the response. The evaluation of the route will provide the contents of the attachment.

Example 2-36. Sending an attachment to a client
require 'sinatra'

before do
  content_type :txt
end

get '/attachment' do
  attachment 'name_your_attachment.txt'
  "Here's what will be sent downstream, in an attachment called 'name_your_attachment.txt'."
end

As always, we’ll take a look at the output via cURL. You’ll note the addition of the Content-Disposition header, which denotes both the fact that it contains an attachment and the optional filename parameter.

$ curl -v http://localhost:4567/attachment
* About to connect() to localhost port 4567 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 4567 (#0)
> GET /attachment HTTP/1.1
> User-Agent: curl/7.19.7 (universal-apple-darwin10.0)
> Host: localhost:4567
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Frame-Options: sameorigin
< X-XSS-Protection: 1; mode=block
< Content-Type: text/plain;charset=utf-8
< Content-Disposition: attachment; filename="name_your_attachment.txt"
< Content-Length: 88
< Connection: keep-alive
< Server: thin 1.2.11 codename Bat-Shit Crazy
<
* Connection #0 to host localhost left intact
* Closing connection #0
Here's what will be sent downstream, in an attachment called 'name_your_attachment.txt'.

Streaming

One of the most exciting new features in Sinatra (version 1.3.0 and up) is support for content streaming from your application. The streaming support abstracts away the differences in different Rack-compatible servers, leaving you to focus solely on developing the functionality you desire as opposed to the plumbing that makes it possible.

Keeping the Connection Open

There are a number of applications that lend themselves to a persistent, open connection. Perhaps you’re developing a chat program or something similar; Example 2-37 shows how to create this type of connection and broadcast messages to subscribers.

Example 2-37. A simple streaming example
require 'sinatra'

before do
  content_type :txt
end

connections = []

get '/consume' do
  stream(:keep_open) do |out|
    # store connection for later on
    connections << out

    # remove connection when closed properly
    out.callback { connections.delete(out) }

    # remove connection when closed due to an error
    out.errback do
      logger.warn 'we just lost a connection!'
      connections.delete(out)
    end
  end
end

get '/broadcast/:message' do
  connections.each do |out| 
    out << "#{Time.now} -> #{params[:message]}" << "
"
  end

  "Sent #{params[:message]} to all clients."
end

It’s a little tricky to demonstrate the behavior in text, but a good demonstration would be to start the application, then open a web browser and navigate to http://localhost:4567/consume. Next, open a terminal and use cURL to send messages to the server.

$ curl http://localhost:4567/broadcast/hello
Sent hello to all clients.

If you look back at the web browser, you should see that the content of the page has been updated with a time stamp and the message that you sent via the terminal. The connection remains open, and the client continues to wait for further information from the server.

Finite Streaming

An alternative to the long-running open connection is to simply keep it open long enough to stream some finite amount of information down to the client; this approach is shown in Example 2-38.

Example 2-38. Streaming a finite amount of information
require 'sinatra'

before do
  content_type :txt
end

get '/har-har' do
  stream do |out|
    out << "Wanna hear a joke about potassium?
"
    sleep 1.5
    out << "K.
"
    sleep 1.5
    out << "I also have one about sodium!
"
    sleep 1.5
    out << "Na.
"
  end
end

Open up a web browser or cURL, make a request to http://localhost:4567/har-har and enjoy!

Warning

We’ve also got some great ones about photons and how on vacation they’re always traveling light.

Summary

This chapter covered the bulk of Sinatra fundamentals: the HTTP verbs, defining routes, halting and redirecting routes, delivering static resources, using filters, and the configuration block. Next, we’ll discuss Sinatra from a much lower level and explore how it really works so that we can make full use of the advanced features it provides.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset