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.
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.
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.
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).
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:
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
DRY stands for “Don’t Repeat Yourself”; it’s also occasionally expressed as DIE, or “Duplication is Evil.”
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
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 in Sinatra can also accept parameters that are exposed in
code via the params
array, as shown
in Example 2-3.
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.
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
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.
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.
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.
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.
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.
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.
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.
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).
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.
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).
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.
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!
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).
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).
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.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Static file</title> </head> <body> <h1>This is a static file.</h1> </body> </html>
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.
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.
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.
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 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.
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, 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.
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.
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.
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.
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.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>External template</title> </head> <body> <h1>Worked!</h1> </body> </html>
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.
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.
If you want to get technical, one could also use #intern
as well.
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.
require 'sinatra' get '/home' do @name = 'Random User' erb :home end
<!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).
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.
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 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.
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).
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.
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.
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.
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.
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.
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?
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.
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).
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.
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.
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
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
A good resource for understanding the various caching options available via HTTP can be found at http://www.mnot.net/cache_docs/.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
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'
.
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).
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.
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 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.
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
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 => '/')
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.
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'.
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.
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.
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.
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.
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!
We’ve also got some great ones about photons and how on vacation they’re always “traveling light.”
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.