This week we started building a Rails 3 application for one of our customers which had to share data with their existing Rails applications, which were built with version 2.1.2 and 2.3.8.
Although session configuration differs from version 2 to 3, getting this done wasn’t such a hard job, mainly thanks to this blogpost written by Dan McNevin.
Basically this means the sessions are configured as follows:
# Rails 2.1.2 # config/environment.rb ... Rails::Initializer.run do |config| config.action_controller.session = { :session_key => '_sso_session', :secret => 'a really long hex string' } config.action_controller.session_store = :cookie_store end ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_domain] = '.rails.local' # End of Rails 2.1.2 # Rails 2.3.8 (and probably 2.3.x) # config/initializers/session_store.rb ActionController::Base.session = { :domain => '.rails.local', :key => '_sso_session', :secret => 'the same really long hex string' } # End of Rails 2.3.8 # Rails 3.0.1 # config/initializers/session_store.rb [AppName]::Application.config.session_store :cookie_store, { :key => '_sso_session', :domain => '.rails.local' } # config/initializers/secret_token.rb [AppName]::Application.config.secret_token = 'the same really long hex string' # End of Rails 3.0.1
Session sharing between 2.1.2 and 2.3.8 worked fine, however when swapping to the 3.0.1 application I got the error:
ActionDispatch::Session::SessionRestoreError (Session contains objects whose class definition isn’t available.
Remember to require the classes for all objects kept in the session.
(Original exception: uninitialized constant ActionController::Flash::FlashHash [NameError])
):
In other words (or actually, my own words): the session contains an object (ActionController::Flash::FlashHash) which is unfamiliar to Rails 3.
To solve this problem, I added the class:
# Rails 3.0.1 # config/initializers/session_store.rb module ActionController module Flash class FlashHash < Hash def method_missing(m, *a, &b) end end end end # End of Rails 3.0.1
Now, the error didn’t show up anymore and so was the session… I was able to switch from Rails 2 to Rails 3, but now the session didn’t contain a single keys!?!?
Assuming there is a require_user method doing the authentication, I added a
# Rails 3.0.1 # app/controllers/application_controller.rb def require_user y request ... # End of Rails 3.0.1
to this controller action (which is short for puts request.to_yaml) and I was surprised to find the keys, which were stored by the Rails 2 app., in the env object in it’s action_dispatch.request.unsigned_session_cookie key:
# Rails 3.0.1 y request.env['action_dispatch.request.unsigned_session_cookie'] # => --- serial: 0 _csrf_token: nTHGmUfA0sKh1rDZWvt+1tLZmG3fCWlhf8pkiHGMU5I= last_quote_time_en_US: !timestamp at: "2010-10-27 14:52:15.736034 +02:00" "@marshal_with_utc_coercion": true session_id: ef4c356efc12113792ecccbde65bba7a user_id: 35 lang: en_US shown_quotes_en_US: - 17688 ... # End of Rails 3.0.1
Pretty hopeless by now, I decided to get the keys I needed out of this Hash and add them to the Rails 3 session manually:
# Rails 3.0.1 session[:user_id] = request.env['action_dispatch.request.unsigned_session_cookie']['user_id'] # End of Rails 3.0.1
But this surprised me even more, finding a fully populated session only after adding one key.
Time to investigate the actionpack gem.
When you add a key to the session object, this will call the []=-method in the ActionDispatch::Session::SessionHash class. The []=-method internally calls a private method load_for_write!. My thinking was (since diving deeper into the code didn’t come to my mind) that the Rails 3 session is fully populated, but not yet loaded when going or returning to the Rails 3 application.
This was an easy one, I just had to reload the session before using it:
# Rails 3.0.1 # app/controllers/application_controller.rb def require_user session.send(:load_for_write!) ... # End of Rails 3.0.1
Problem solved? Well, not completely. I was able to successfully browse from the Rails 2 app. to Rails 3, without losing my session, but going back to the Rails 2 app. introduced another problem: keys initially stored as symbols were now turned into string because of the Rails 3 app.
Initially, I tried to solve this by converting all keys back into symbols, but this should introduce another problem, since I was not sure if all session keys were stored as symbols. Rails itself stores the Flash object in the session into the “flash” key, instead of :flash.
A better approach is to patch the CGI::Session object and make sure all keys can be stored and retrieved as both string and symbols:
# Rails 2.1.2 and 2.3.x # config/initializers/load_patches.rb # # Loads patches stored in lib/patches. Dir[RAILS_ROOT + "/lib/patches/**/*.rb"].each { |file| require file } # lib/patches/cgi/session.rb require 'cgi/session' # Patching CGI:Session so that it on longer matters if you retrieve a session # value using a String, Symbol, ... This in order to make it play nicely # together with Rails 3. # # = Examples # # session[:foo] = "Bar" # session[:foo] # => "Bar" # session["foo"] # => "Bar" # # session["qux"] = "Baz" # session[:qux] # => "Baz" # session["qux"] # => "Baz" class CGI #:nodoc: class Session #:nodoc: def [](key) @data ||= @dbman.restore @data[key.to_s] end def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @data[key.to_s] = val end end end # End of Rails 2.1.2 and 2.3.x
Now it worked! I am able to share sessions between Rails 2.1, 2.3.x and Rails 3.0 application.
To wrap things up:
# Rails 2.1.2 # config/environment.rb ... Rails::Initializer.run do |config| config.action_controller.session = { :session_key => '_sso_session', :secret => 'a really long hex string' } config.action_controller.session_store = :cookie_store end ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_domain] = '.rails.local' # config/initializers/load_patches.rb Dir[RAILS_ROOT + "/lib/patches/**/*.rb"].each { |file| require file } # lib/patches/cgi/session.rb require 'cgi/session' class CGI #:nodoc: class Session #:nodoc: def [](key) @data ||= @dbman.restore @data[key.to_s] end def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @data[key.to_s] = val end end end # End of Rails 2.1.2 # Rails 2.3.8 (and probably 2.3.x) # config/initializers/session_store.rb ActionController::Base.session = { :domain => '.rails.local', :key => '_sso_session', :secret => 'the same really long hex string' } # config/initializers/load_patches.rb Dir[RAILS_ROOT + "/lib/patches/**/*.rb"].each { |file| require file } # lib/patches/cgi/session.rb require 'cgi/session' class CGI #:nodoc: class Session #:nodoc: def [](key) @data ||= @dbman.restore @data[key.to_s] end def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @data[key.to_s] = val end end end # End of Rails 2.3.8 # Rails 3.0.1 # config/initializers/session_store.rb [AppName]::Application.config.session_store :cookie_store, { :key => '_sso_session', :domain => '.rails.local' } module ActionController module Flash class FlashHash < Hash def method_missing(m, *a, &b); end end end end # config/initializers/secret_token.rb [AppName]::Application.config.secret_token = 'the same really long hex string' # app/controllers/application_controller.rb def require_user session.send(:load_for_write!) ... # End of Rails 3.0.1
Next job is to upgrade the legacy code to Rails 3…

