Fun fact: HTTP is a stateless protocol. Bust that one out at the next party you attend, and see if anyone is impressed. (If anyone is, you might be at the right party!) In all seriousness, this is an important one – in fact, statelessness is a concept that is the basis for all things web development. Let's break it down.
We've already seen how web applications are delivered to the end-user via a request-response cycle; that is, the web browser sends a request to the web server, and the web server responds with HTML (and perhaps CSS/JS). What we haven't talked about yet is the fact that each of the request-response cycles performed throughout the end-user's time using our application are completely independent from each other. Consider the journey of an end-user using a e-commerce bookstore application we've built:
- The user visits
/books
to browse a list of books for sale - The user clicks on a a particular title, which takes them to
/books/123
where the details of the book are displayed - The user clicks on the "My Orders" tab, which directs them to
/orders
where a list of their orders is shown - The user clicks on one of their open orders, which goes to
/orders/42
where the details and status of order #42 can be viewed
Each step of this journey involves a separate request to the web server and subsequent response. However, request #2 doesn't know that we came from request #1, request #3 has no idea about request #2, and so on. Each of these request-response cycles happen independently of each other, and the web application doesn't really even know (or care) about the order in which they happened. By the time the end-user is looking at a past order, the application has no idea that their trip originated at browsing through the book catalog.
Said another way – the web browser doesn't maintain any state between requests. HTTP is a stateless protocol.
Well, that sucks.
But we need our web application to be able to maintain state, right? That's the user experience we've come to expect from web applications. For example, a typical web application:
- Knows who is using the application (i.e. logged-in)
- Knows what the user did earlier (for example, added something to a shopping cart)
- Is perfectly capable of displaying dynamic content based on the user's journey (for example, showing a "thank you" message if the user places an order)
So how does this happen if HTTP is stateless and doesn't know what the user did before or the path they have taken? The answer: we have to trick the browser into thinking it does indeed have state. There are a couple of main ways that web developers do this today – in this lesson, we'll start with the most basic way and work our way forward in a future lesson.
The Query String
Using a query string is a very popular way of passing data to a request. To see how it works, let's try visiting the following URL:
https://google.com/maps?q=paris
We'll see that this takes us to Google Maps, with Paris, France at the center of the map. Now, let's go to a similar, but slightly different URL:
https://google.com/maps?q=las+vegas
We'll see that it's still Google Maps, but puts us in the heart of Las Vegas instead. If we look carefully at these two URLs, we'll notice that the protocol (https), host/domain name (google.com), and resource/path (/maps) are identical. The part that's different is the part after the ?q=
– either paris
or las+vegas
. The part of the URL after the question mark ?
is known as the query string. Let's have one more example:
https://google.com/maps?q=paris&hl=fr
Here we see a query string with two pieces of information – also known as the two query string parameters – separated by the &
symbol. In Google parlance, q
is the "query" and hl
is the "host language" – so with this particular query string, we're passing two pieces of data to Google Maps – the first is that we'd like to search for "paris" and secondly, we'd like the results to be in French. Of course, it's still completely up to the Google Maps server application to decide what to do with these query string parameters – but in this case, it works.
At this point, a keen observer might notice that the query string parameters are a set of key-value pairs. Sound familiar? What else do we know of that is a set of key-value pairs? Let's explore further, this time in our own Rails application.
We're going to supply our dice resource with data, much like we supplied Google Maps with data in the previous example. Let's open the page to roll the dice again, but this time, let's add a bit of data to the URL and visit /dice?name=Brian
.
Note that the output is no different than it was before, despite sending data via the query string. But this is only because our server application doesn't do anything with the data it's given! For the first time, we're going to have a look at our Rails server log. Where is the server log? The complete log lives at log/development.log
, but you also see it within the output of our rails server
command. The server log provides us with extremely valuable information about what's going under the hood of our application, and we should start to become comfortable looking at it often. If we look at the server log output for this particular request, we're going to see something similar to this:
Started GET "/dice?name=Brian" for <IP ADDRESS> at <DATETIME>
Processing by DiceController#index as HTML
Parameters: {"name"=>"Brian"}
Rendering layout layouts/application.html.erb
Rendering dice/index.html.erb within layouts/application
Everything we'd want to know about how our application is receiving the request is shown right here in the server log. We can see the exact URL path, which controller is processing the request, and – most importantly in the context of this lesson – any data being passed along with the request.
The params
Hash
The data being received by our request is so important, in fact, that it lives in a special place.
Parameters: {"name"=>"Brian"}
Remember that whole thing about key-value pairs? That's right, the key-value pairs supplied in the URL – in this case, name=Brian
– are being translated into a Ruby Hash. And this Hash has a special name – params
. So if we wanted to grab the name "Brian" that's being given to us in the URL, we'd simply pull it out of the hash:
params["name"]
Let's use it in our dice application! In our dice controller, let's grab that value and assign it to a variable that we can use in our view:
def index @die1 = rand(1..6) @die2 = rand(1..6) @total = @die1 + @die2 @name = params["name"] end
Then, in our view, change the first line so that it reads:
<h1>Roll the dice, <%= @name %>!</h1>
Refresh the browser (making sure the path is still /dice?name=Brian
) and see the result. And, try changing the name parameter in the URL to see how the page changes. Our page is now accepting the data from the URL's query string and using it to dynamically alter our resulting HTML. Cool!
To roll again, hit the "Roll Again" link on the page... uh-oh.
What happened?! The URL tells us everything we need to know – we can see that the path is now /dice
and contains no query string parameters. Why? Because HTTP is stateless. Unless we do the job of taking the data passed to the current request and sending it along to the subsequent request, it doesn't happen automatically. Indeed, we've got to modify our link to "Roll Again" in the view in order to make that happen:
<a href="/dice?name=<%= @name %>">Roll Again</a>
Visit /dice?name=Brian
again, and we'll see the successful transfer of "state" from one page to the next.
Completing the Game
Of course, the game only works properly at the moment if a name is initially supplied in the URL – again, not a great user experience. It would be better if we could ask for a name, in the event that the player's name is not supplied in the URL. The following, completed code shows us a way we could do that:
Cookies
The query string was very much "the way" to persist state across request-response cycles for a very long time on the web. However, the technique comes with a rather large problem – it's too easily hacked. After all, if all the data being transmitted with a new request simply lives in the URL, then all a user needs to do to potentially completely change the application's behavior is to mess with the URL. Eventually, browsers started to support other ways of storing data locally – such as cookies and lightweight databases.
However, this is a discussion for another day. We'll revisit this topic when we discuss application security and user authentication in a future lesson.
POST Parameters
Another, more secure way to pass information from one page to the next is via POST parameters, which is what we'll talk about in the next lesson.