
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…