Most web applications need to store users' data, such as their username, email, or their preferences across multiple requests. In addition, once a user logs into your application, they need to remain logged in until they log out.
However, HTTP is a stateless protocol. In simple words, the server does not retain any information from one request to another. So how and where do we store this user-specific data that we need across multiple requests? The answer is: a session.
Sessions provide a way to store a user's information across multiple requests. Once you store some data in the session, you can access it in all future requests.
# store the username in the session
# when a user logs in
session[:user_id] = user.username
# read the username from the session
# when the user accesses the website again
username = session[:user_id]
Looks simple, right? Well, there's a lot that goes on behind the scenes to make it work. This article will show you how sessions work and how you can work with them.
What You'll Learn
- What is a session?
- How Sessions Work
- Session Storage
- Working with Sessions
- How Rails Implements Sessions
- Resources to Learn More
Sounds fun? Let's dig in.
What is a Session?
A session can refer to one of two things:
- A place where you store data during one request that you can access in later requests.
- A time period between which the user is logged in to your application.
Once a user logs in, the application stores their information in a session (the place), and uses that information in subsequent requests to make sure the user is valid and logged in (the period).
Your application has a session for each user in which you can store small amounts of data that will be persisted between requests.
You can write some data to the session in one request:
class SessionsController < ApplicationController
def store
session[:user_id] = user.username
end
end
and read it inside another request:
class SessionsController < ApplicationController
def read
username = session[:user_id]
end
end
As you can see, Rails makes it extremely easy for you to work with sessions. You can simply treat it like a Ruby Hash.
How do Sessions Work?
At this point, you may be wondering: how do sessions help you maintain the state from one request to another?
The answer lies in a cookie.
When your browser requests a website for the first time, along with the response, the server sends a small piece of data back to the web browser. This data is called a cookie. Upon receiving this cookie, the browser stores this cookie and sends it back to the server for all future requests.
If you're curious to learn more about cookies, I recommend reading the MDN guide: Using HTTP cookies. In a nutshell, the server can set cookies using the Set-Cookie header and read cookies using the Cookie header.
The server sets a different cookie for different browsers, even if they're on the same computer. That's why cookies are typically used to tell if multiple requests came from the same browser. This lets the website remember useful information from previous requests (such as the user's unique identifier), despite HTTP being a stateless protocol.
Example: how you stay logged in on Amazon
Let's say you're logging in to Amazon for the first time. Once you log in, Amazon's back-end server will send a cookie in the response, and this cookie will be sent to the server every time you browse any pages on Amazon.
Upon receiving the cookie, Amazon's server checks if the user corresponding to that cookie is logged in, and displays a personalized web page.
However, if you browse Amazon from a different device or even a different browser, it won't send the cookie to the server, as it doesn't have that cookie. Cookies are scoped to the browser. Hence you won't be logged in on a different device or browser.
So, how does that relate to the Sessions?
Sessions use cookies behind the scenes. Instead of you having to set and get data from the cookies, Rails handles it for you via sessions. Your Rails application has a session for each user, in which you can store data that will be persisted between multiple requests.
Rails identifies the session by a unique session ID, a 32-character string of random hex numbers. When a new user accesses the application for the first time, Rails generates this session ID and stores it inside the cookie. Once this session ID is sent to the browser, it sends it back to the server for future requests.
Session Storage
When you store some data in the session, Rails can store it in any one of the following four session stores.
CookieStore
: Store all data inside a cookie, in addition to the session IDCacheStore
: Store data inside the Rails cacheActiveRecordStore
: Store the data in the databaseMemCacheStore
: Use a Memcached cluster of servers
All these stores can be accessed via a unified API, making it very convenient to switch from one to another.
One important thing to note here is that whichever store you use to store the session data, the actual session identifier always comes from the cookie. I feel this is a point that confuses many beginner Rails developers, and it confused me for a long time.
The server communicates the state with the browser by sending the session ID via a cookie. This cookie is used for all future requests to identify the session for that particular user, and to store additional data in any one of the session stores mentioned above.
For example, you can store that user's preferences inside the cookie (in addition to the session id), in the database, or the cache.
The best practice is to use the cookie only to store a unique ID for each session, and nothing else.
Cookies have a size limit of 4 KB. Since the browser sends cookies on each request, stuffing large data in cookies will bloat the requests. Hence you should only store the session ID in the cookie. Upon receiving the request, your application can use that identifier to fetch additional data from the database, cache, or Memcached.
Working with Sessions
I hope that by now you should have a solid understanding of what sessions are, why we need them, and how they work on a high level. In this section, we will explore how you can work with sessions in Rails on a day-to-day basis.
You can access sessions in only two places: controllers and views. Since a session (and cookies) is a concept related to HTTP, it doesn't make sense to access them inside your models, services, or anywhere else. The controllers are the entry point for all incoming HTTP requests and make the best place to access sessions.
There are two primary ways to work with session data in Rails.
- Use the
session
instance method available in the controllers. It returns a Hash-like object, and you can use it just like a regular Hash in Ruby. - Use the session method on the
request
objects, which is useful if you need to access sessions inside your routes file.
Let's take a look at accessing sessions via the session
method in the controllers. This is the most common way you'll be accessing sessions in your Rails applications.
class ProjectsController < ApplicationController
def show
# read data
value = session[:key]
name = session[:name]
# write data
session[:key] = value
session[:name] = 'Akshay'
end
end
In addition to the standard hash syntax in Ruby, you can also use the fetch
method on the session. This lets you pass a default value while trying to read an item from the session. If the specified key doesn't exist in the session, Rails will return the default value you provided.
class ProjectsController < ApplicationController
def show
session["one"] = "1"
session.fetch(:one) # 1
session.fetch(:two, "2") # 2
session.fetch(:two, nil) # nil
end
end
You can also pass a block to the fetch
method. This block will be executed and its result will be returned if the specified key doesn't exist. The key is yielded to the block.
class ProjectsController < ApplicationController
def show
session.fetch(:three) { |el| el.to_s } # "three"
end
end
If you don't pass a default value or a block, Rails will raise an error.
class ProjectsController < ApplicationController
def show
session.fetch(:three) # raises KeyError
end
end
Use the dig
method if you have nested data in the session.
class ProjectsController < ApplicationController
def show
session["one"] = { "two" => "3" }
session.dig("one", "two") # 3
session.dig(:one, "two") # 3
session.dig("one", :two) # nil
end
end
Update and Delete the Session Data
If you've already stored some data in the session and would like to update it, use the update
method, providing the updated value.
class ProjectsController < Application Controller
def new
session["action"] = "NEW"
end
def create
session.update(action: "CREATE")
end
end
If you decide you don't need to keep some data in the session anymore, such as a user's ID aftere they log out, use the delete
method to remove it.
class ProjectsController < Application Controller
def new
session.delete("action")
end
end
Accessing Sessions Inside Routes
Sometimes, you may need to access sessions inside your routes.rb
file. For this, you can call the session
method on the request
object available inside the route constraints.
get 'sessions', to: 'projects#sessions', constraints: ->(req) {
req.session[:name] = 'akshay'
true
}, as: 'sessions'
Alternatively, you can create an instance of the ActionDispatch::Request
using the env
object yielded to the Rack application.
get 'sessions', to: -> (env) {
req = ActionDispatch::Request.new(env)
req.session[:name] = 'Akshay'
[200, {}, ["Success!"]]
}
To learn more about router constraints and using a rack app inside the routes, check out this article that goes deep into routing in Rails.
One final thing to remember: Sessions are lazily loaded. If you don't access sessions at all, they will not be loaded. Hence, you don't have to disable sessions if you aren't using them.
Digging Deeper: How Rails Implements Sessions
So far, you've learned that the session
instance method provides access to the session inside the controller. Ever wondered where that method comes from?
Let's open the Rails codebase and take a peek at the ActionController::Metal
class, the great-grandfather of all your controllers.
Here's a simplified version:
module ActionController
class Metal
delegate :session, to: "@_request"
end
end
The delegate
method forwards any calls to session
to the @_request
object, which is an instance of the ActionDispatch::Request
class. Let's open it and see if we can find the session
method in this class.
We can't!! There's no session
method inside the ActionDispatch::Request
class.
What's going on? 🤔
Let's see if it includes any modules. Yes, it does:
module ActionDispatch
class Request
include Rack::Request::Helpers
end
end
Let's open the Rack codebase, go to the Rack::Request class, and there is our session
method! All it does is check the value of the RACK_SESSION
header variable and set it to the value returned by the default_session
method.
# https://github.com/rack/rack/blob/main/lib/rack/request.rb
module Rack
class Request
module Helpers
def session
fetch_header(RACK_SESSION) do |k|
set_header RACK_SESSION, default_session
end
end
def default_session; {}; end
end
end
end
Let's get back to the Rails codebase. The ActionDispatch::Request
class overrides the default_session
method, providing its own implementation as follows:
module ActionDispatch
class Request
def default_session
Session.disabled(self)
end
end
end
The Session.disabled
method returns a new instance of the ActionDispatch::Request::Session
class. Here's a simplified version of it.
module ActionDispatch
class Request
class Session
def self.disabled(req)
new(nil, req, enabled: false)
end
end
end
end
And that's where the session object is created. We can go deeper, but I think we have a pretty good foundation so you can explore the Session
class on your own, which I highly encourage. In a future article, we'll explore how different session stores are implemented.
Resources
- How Rails Sessions Work: This is the original blog post from Justin Weiss, that made the sessions 'click' for me. While you're there, I recommend reading other articles from Justin.
- Rails Guides on Sessions: Read them for more details on configuring different stores for the sessions, also tangential concepts such as the flash.
- Laravel Sessions: To learn how Laravel uses sessions. Rails and Laravel are very similar, and I always learn new things by reading the Laravel docs.
I hope this post helped you gain a deeper understanding of sessions in Rails. Typically, you'll use sessions to implement authentication for your application. However, you can also use them to store additional data such as user preferences or UI/theme settings for the website.
If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.