Post

Rails Headers

Here are two tech articles based on your request.


Article 1: Cracking the Code: How Rails, Warden, and Cookies Handle Your Session

When a user logs into a Rails app, they magically stay logged in across multiple requests. It feels simple, but beneath the surface is a coordinated dance between the browser, your Rails application, and a key piece of middleware called Warden.

Let’s pull back the curtain and see how it all works, including how to test it correctly.


The Key Players

First, let’s meet the cast:

  • The Browser Cookie: Think of this as a simple ticket stub. The server gives it to the browser, and the browser shows it back to the server on every subsequent visit. It holds a single piece of encrypted, signed data: the entire session hash.
  • The Rails Session Store: This is the server-side “brain.” By default, Rails uses ActionDispatch::Session::CookieStore, which means it doesn’t store session data on the server at all. Instead, it encrypts the session data and stuffs it into the cookie. This is why it’s called a “cookie store.”
  • Warden: This is the “bouncer.” It’s a Rack middleware that provides a flexible authentication framework. Warden doesn’t manage the session itself; its job is to check authentication credentials and then tell the Rails session, “Hey, this user is authenticated. Remember them,” or “This user is logging out. Forget them.”

The Interaction Flow

The best way to understand the relationship is to follow a request. The diagram below shows how a request from a logged-in user is handled.

graph TD
    subgraph "User's Browser"
        A[Browser]
    end

    subgraph "Ruby on Rails Application"
        B[Rails Middleware Stack]
        C[Warden Middleware]
        D[Session Store Middleware <br> (ActionDispatch::Session::CookieStore)]
        E[Application Controller]
        F[Database]
    end

    A -- "1. Request with Session Cookie" --> B
    B -- "2. Middleware passes request to Session Store" --> D
    D -- "3. Decrypts Cookie & provides session hash" --> C
    C -- "4. Warden checks session for auth key" --> D
    C -- "5. Deserializes user ID from session" --> F
    F -- "6. Returns full User object" --> C
    C -- "7. User Authenticated! <br> `warden.user` is now set" --> E
    E -- "8. Response is sent back up the stack" --> B
    B -- "9. Session Store sees no changes, does nothing to the cookie" --> D
    B -- "10. HTTP Response sent to Browser" --> A

Step-by-Step Breakdown:

  1. The browser sends a request containing the session cookie.
  2. The Rails middleware stack begins processing the request.
  3. The Session::CookieStore middleware decrypts the cookie’s content and loads it into the session hash, making it available to the rest of the app.
  4. Warden middleware runs. It inspects the session hash for its specific key (e.g., warden.user.user.key).
  5. If the key exists, Warden takes the ID from the session (e.g., 123) and asks the database for that user. This is called deserialization.
  6. The database returns the full user object (User.find(123)).
  7. Warden authenticates the request and makes the user object available to your controller via warden.user.
  8. When a user logs in (e.g., in a SessionsController#create action), you’ll call warden.set_user(user). This triggers Warden to perform serialization, putting the user’s ID back into the session hash.
  9. On the way out, the Session::CookieStore middleware sees that the session hash has changed (because a user just logged in).
  10. It re-encrypts the entire updated session hash and puts it in the Set-Cookie header of the response sent back to the browser.

Testing the Flow: Choosing the Right Spec

Because different parts of the stack are responsible for different jobs, you must choose the right test type.

Controller Specs

Controller specs are for testing the logic inside a single controller action in isolation. They do not run the full middleware stack.

  • What you CAN test: That your action caused the session hash to be correctly modified. You test the cause, not the effect.
  • What you CANNOT test: The raw Set-Cookie header in the response, because the middleware that creates it never runs.
1
2
3
4
5
6
7
8
9
10
11
# spec/controllers/sessions_controller_spec.rb
RSpec.describe SessionsController, type: :controller do
  it "populates the session with the user's key on login" do
    user = create(:user)
    post :create, params: { email: user.email, password: 'password' }

    # Test the session hash directly. This is the correct way.
    warden_key = session['warden.user.user.key']
    expect(warden_key.first.first).to eq(user.id)
  end
end

Request Specs

Request specs (integration tests) are for testing the application’s behavior through the entire stack, from routing to the response. They behave like a browser without a UI.

  • What you CAN test: Everything from the controller spec, plus the final HTTP response, including status codes and headers like Set-Cookie.
1
2
3
4
5
6
7
8
9
10
11
# spec/requests/sessions_spec.rb
RSpec.describe "Sessions", type: :request do
  it "sets the session cookie in the response header on login" do
    user = create(:user)
    post login_path, params: { email: user.email, password: 'password' }

    # You can inspect the actual headers because the middleware ran.
    session_cookie_name = "_your_app_name_session"
    expect(response.headers['Set-Cookie']).to include(session_cookie_name)
  end
end

System Specs (End-to-End)

System specs drive a real (or headless) browser to test a user’s journey from start to finish.

  • What you SHOULD test: The user-visible outcome. You don’t need to check the session or cookies at all. You just verify that the result of being logged in is present. This implicitly confirms the entire stack is working.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# spec/system/login_spec.rb
RSpec.describe "Login", type: :system do
  it "allows a user to log in and see the dashboard" do
    user = create(:user)
    
    visit login_path
    fill_in "Email", with: user.email
    fill_in "Password", with: "password"
    click_button "Log In"

    # Test the outcome. This is the best end-to-end test.
    expect(page).to have_content("Welcome back, #{user.name}")
    expect(page).to have_current_path(dashboard_path)
  end
end

Article 2: Custom HTTP Headers in Rails: Choosing the Right Header for Your API

When designing an API in Rails, you often need to pass custom metadata. A common case is an alternative authentication mechanism, like an API token. You decide to use a header, but which format is correct?

  • HTTP_X_AUTH_METHOD
  • X-AUTH-METHOD
  • ACCEPT

Let’s clarify the confusion and establish the modern best practice.


How Rails Reads HTTP Headers

First, you must understand a critical translation step. When a client sends an HTTP request, the webserver (like Puma) and Rack (the interface to Rails) normalize the headers before your application sees them. The rules are:

  1. They prepend HTTP_ to the header name.
  2. They convert all hyphens (-) to underscores (_).
  3. They uppercase the entire name.

For example, if a client sends a header like Auth-Method: token, your Rails controller will access it as request.headers['HTTP_AUTH_METHOD'].

This single piece of knowledge is key to debugging and understanding custom headers.


Deconstructing the Header Options

Now let’s evaluate our three choices.

🏛️ ACCEPT

The Accept header has a single, well-defined purpose: Content Negotiation. The client uses it to tell the server what kind of content format it can understand.

  • Example: Accept: application/json tells the server, “I want a JSON response.” Accept: text/html says, “I want a full HTML page.”
  • Verdict: Never use Accept for authentication. Overloading it for a custom purpose violates the HTTP specification, breaks caching mechanisms, and will deeply confuse other developers and tools that rely on its standard behavior.

👴 X-AUTH-METHOD (The “X-“ Prefix)

The X- prefix was historically used to signify a non-standard, “experimental” header. The idea was to avoid clashes with future standard HTTP headers.

However, this practice was officially deprecated in 2012 by RFC 6648. The practice became problematic because many X- headers (like X-Forwarded-For) became de facto standards, making a future transition to a standard header without the X- painful.

  • Verdict: The X- prefix is legacy. While it works, you should avoid using it in new applications. It signals an outdated design.

✅ The Modern Approach: A Descriptive Name

The current best practice is simple: name your header descriptively without any special prefixes. If you want to specify an authentication method, a great name is simply Auth-Method.

  • Client Sends: Auth-Method: token
  • Rails Accesses: request.headers['HTTP_AUTH_METHOD']
  • Verdict: This is the recommended approach. It’s clean, compliant with modern standards, and self-documenting.

The Winner and Why

For a new custom header, you should use a simple, descriptive name without a prefix.

Client SendsRails Accesses ViaPurposeRecommendation
Accept: tokenrequest.headers['HTTP_ACCEPT']Content NegotiationWrong. Do not use.
X-Auth-Method: tokenrequest.headers['HTTP_X_AUTH_METHOD']Custom (Legacy)Avoid. Deprecated practice.
Auth-Method: tokenrequest.headers['HTTP_AUTH_METHOD']Custom (Modern)Correct. Use this.

By choosing a simple name like Auth-Method, you create an API that is clear, modern, and aligned with internet standards, ensuring it’s easily understood by developers and tools alike.

This post is licensed under CC BY 4.0 by the author.