Patrick Baselier – 27 October 2010
1050 words in about 5 minutes

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 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:

1
2
3
4
5
6
7
8
9
10
11
# 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

1
2
3
4
5
6
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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:

1
2
3
# 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:

1
2
3
4
5
6
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 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…

Patrick Baselier

I’m a professional Ruby on Rails-, front-end- and unprofessional (that is: not professionally… yet) Ember developer from The Netherlands, I love sharing knowledge and one day I hope to be a more than a novice guitar player.