I'm experiencing issues with an elastic load balancer and varnish cache with respect to cookies and sessions getting mixed up between rails and the client. Part of the problem is, rails is adding a "Set-Cookie" header on with a session id on almost every request. If the client already is sending session_id, and it matches the session_id that rails is going to set.. why would rails continuously tell clients "oh yeah.. you're session id is ..."
Summary: Set-Cookie
headers are set on almost every response, because
Set-Cookie
headers.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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With