I recently worked in a Rails project with Peter (@sporkd). The project is intended to be used as a sub-site, and should be served under sub-URI. After google, we ended up by setting config.assets.prefix
and wrapped all routes in scope
. The solution is simple and worked well. But soon, some weird bugs were found, and Peter was successfully isolated the problem to session (see demo sporkd/asset_prefix_test)
After several hours debugging, I finally found out the cause. To make a long story short, the routes configured in routes.rb
should not start with config.assets.prefix
, otherwise session save is skipped. The demo sporkd/asset_prefix_test
can be fixed by simply setting config.assets.prefix
to /one/assets
. You also get a bonus by setting a unique prefix for assets, since it is easy to add expiration header for assets in web server.
If you are curious why config.assets.prefix
can affect session and want to know some internals about the X-Cascade header in Rails, please read on.
X-Cascade Header in Rails
I never knew X-Cascade
header in Rails before. @soulcutter has a post that described its usage.
The basic idea is this: if you return a response from a controller with the X-Cascade header set to “pass”, it indicates that your controller thinks something else should handle the request. So rails (or is it rack? in any case…) will continue down your routes looking for the next rule that matches the request.
Indeed, X-Cascade
is not only restricted in controller, if a mounted engine sets this header, Rails also continues down the routes searching.
It is a feature of Rails. Since 3.2, Rails has moved the routes logic to journey. The X-Cascade
trick can be found in journey/router.rb#L69.
Pay attention that, the rack env
object is shared when request is passed on. So if env
is changed by former route, the latter one is affected. This is the root cause of the weird session issue, because session is controlled by env['rack.session.options']
.
Sprockets, who skips the session
Sprockets, the gem for rails assets pipeline, mounts itself on config.assets.prefix
and prepends the route to Rails. So if user accesses a page which path starts with config.assets.prefix
, sprockets always processes the request first.
Maybe for performance, sprockets disables session save by changing env['rack.session.options']
:
env['rack.session.options'] ||= {} env['rack.session.options'][:defer] = true env['rack.session.options'][:skip] = true
The options are changed even when asset is not found. If so, sprockets returns 404 and sets the header X-Cascade
. Then Rails passes the request to controller, and correct page is rendered as expected. However, since the session is already disabled by sprockets, the changed session in controller is never saved.
Because env
is a shared resource between routes when X-Cascade
is set, it should not be changed unless it has to. When asset is not found, sprockets should just pass though without touching env
, so I submit a PR for it.
How we Debug
Peter and I worked in different time zones. He first found the session issue because several features related to session did not work. He made the demo sporkd/asset_prefix_test
to isolate the issue using minimum code at the end of the day in his time zone and left me the message.
When my day started, I got the message and started debugging on session based on the demo in doitian/asset_prefix_test.
Because session store class is customizable, I inherited one from default cookie store and added breakpoints using pry. Soon I found out that options[:skip]
was true
, but I had no idea where it was set to true
. Then I did a grep (using ag) in all gems, and fortunately, only sprockets has set this option to true
. The remaining work was just figuring out why sprockets is invoked before controller action.