Rails Assets Prefix May Disable your Session

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.