Why is rails constantly sending back a Set-Cookie header?
See the answer by @georgebrock on why this happens. It's pretty easy to patch rails to change this behaviour to only set the cookie if the session changes. Just drop this code in the initializers directory.
require 'rack/session/abstract/id' # defeat autoloading
module ActionDispatch
class Request
class Session # :nodoc:
def changed?;@changed;end
def load_for_write!
load! unless loaded?
@changed = true
end
end
end
end
module Rack
module Session
module Abstract
class Persisted
private
def commit_session?(req, session, options)
if options[:skip]
false
else
has_session = session.changed? || forced_session_update?(session, options)
has_session && security_matches?(req, options)
end
end
end
end
end
end
Summary: Set-Cookie
headers are set on almost every response, because
- the default session store will try to write the session data to an encrypted cookie on any request that has accessed the session (either to read from it or write to it),
- the encrypted value changes even when the plain text value hasn't,
- the encryption happens before it reaches the code that's responsible for checking if a cookie value has changed to avoid redundant
Set-Cookie
headers.
Plain-text cookies
In Rails, the ActionDispatch::Cookies
middleware is responsible for writing Set-Cookie
response headers based on the contents of a ActionDispatch::Cookies::CookieJar
.
The normal behaviour is what you'd expect: if a cookie's value hasn't changed from what was in the request's Cookie
header, and the expiry date isn't being updated, then Rails won't send a new Set-Cookie
header in the response.
This is taken care of by a conditional in CookieJar#[]=
which compares the value already stored in the cookie jar against the new value that's being written.
Encrypted cookies
To handle encrypted cookies, Rails provides an ActionDispatch::Cookies::EncryptedCookieJar
class.
The EncryptedCookieJar
relies on ActiveSupport::MessageEncryptor
to provide the encryption and decryption, which uses a random initialisation vector every time it's called. This means it's almost guaranteed to return a different encrypted string even when it's given the same plain text string. In other words, if I decrypt my session data, and then re-encrypt it, I'll end up with a different string to the one I started with.
The EncryptedCookieJar
doesn't do very much: it wraps a regular CookieJar
, and just provides encryption as data goes in, and decryption as data comes back out. This means that the CookieJar#[]=
method is still responsible for checking if a cookie's value has changed, and it doesn't even know the value it's been given is encrypted.
These two properties of the EncryptedCookieJar
explain why setting an encrypted cookie without changing its value will always result in a Set-Cookie
header.
The session store
Rails provides different session stores. Most of them store the session data on a server (e.g. in memcached), but the default— ActionDispatch::Session::CookieStore
—uses EncryptedCookieJar
to store all of the data in an encrypted cookie.
ActionDispatch::Session::CookieStore
inherits a #commit_session?
method from Rack::Session::Abstract::Persisted
, which determines if the cookie should be set. If the session's been loaded, then the answer is pretty much always “yes, set the cookie”.
As we've already seen, in the cases where the session's been loaded but not changed we're still going to end up with a different encrypted value, and therefore a Set-Cookie
header.