Share sessions between Rails 2 and Rails 3 applications

Patrickbaselier

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…

Patrickbaselier

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.

At Kabisa, privacy is of the greatest importance. We think it is important that the data our visitors leave behind is handled with care. For example, you will not find tracking cookies from third parties such as Facebook, Hotjar or Hubspot on our website. Only cookies from Google and Vimeo are used in order to improve the user experience of our visitors. These cookies also ensure that relevant advertisements are displayed. Read more about the use of cookies in our privacy statement.