In my last post I briefly described some of the RSoC changes and plans. One of the things that I left is router. The topic is much harder and I think it deserves separate blog post.
Basically, the problem with engines and routes is that now you can have more than one router. That leads to many issues that need to be solved. As with my last post, things described here are not in Rails core yet, so you will have to wait a bit to actually use it.
For the good start, let’s identify things to consider:
At the beginning I would like to explain how does recognition work. Here is a simple example of engine mounted in application:
As you can see engine provides :posts resource and it’s mounted at /blog path. Let’s study simple request to our blog:
Application’s router will match the first part of the path, which is
/blog mount point and pass the request to rack app mounted there (which in that case is
Blog::Engine). To allow engine recognizing path properly it needs to pass
/posts as a path. The
/blog (which I will call prefix later on) is not a part of mounted engine, but we don’t want to loose that information by simply removing it from PATH. In that case
/blog prefix is attached to
SCRIPT_NAME is part of rack’s spec). That way, engine will get only
/posts part as PATH, which will allow to properly recognize it.
The only one problem with recognition is connected with rack middlewares. Consider such example:
What will happen when you will request
/blog/omg path? It will call the
Blog::Engine first, as it has higher priority than
/blog/omg. Request will pass through the Engine’s middleware stack firing SomeMiddleware and hit the Engine’s router. If
/omg is not a valid route for
Blog::Engine it will return 404 and Application’s router will try next route, that is
/blog/omg pointing to
OmgController. Middleware stack for that controller also includes
SomeMiddleware so it will be fired again, which is probably not something that we want.
The solution is to mount apps with lower priority than other routes.
The problem with generation is related to mount point, the place where engine is mounted. Consider such example:
Blog::Engine is mounted at
"/:user/blog" (let’s just suppose that blog implements multi user setup) and it provides
posts resources. As you can remember from recognition part,
/:user/blog part is engine’s prefix. Additionally, default user is set to “drogus”, so when :user is not specified it will be set to drogus. Imagine that you need to generate path to posts. Something that you would normally achieve by calling
posts_path method. With more than one router the situation is not so simple. We can’t simply use named url helpers (like
posts_path) inapplication’s controllers, so we need some other way to handle that (if you’re wondering why can’t we just use
posts_path, it’s explained in named routes section of this post).
First problem connected with that is API. Jeremy Kemper came up with helper that will allow using mounted engine’s url helpers by simply calling
some_engine.posts_path. The name of that helper is taken from Engine’s name or from
:as option used in mount method. Because
:as option is not provided in my example, helper’s name will be
blog_engine (based on
Blog::Engine). Using that helper we can generate paths for Engine:
We can also generate application’s urls with
The next problem to solve is behavior of such helpers. At first, it seems that it’s fairly easy, but in reality there is quite a few cases to handle. The thing that is most important while considering url generation is context. We can generate urls in application’s controllers, engine’s controller’s, ActionMailer or regular classes not related to Rails itself. Let’s go through each case.
Generating posts path inside one of application controllers (and consequently views and helpers) should generate prefix according to mount point. In that particular situation, default user will be inserted in place of :user, so the url will be
"/drogus/blog/posts". You could also do
blog_engine.posts_path(:user => "john"), which would generate
This situation is a bit different. The first thing you will probably notice is that I used posts_path instead of blog_engine.posts_path. This is possible because I included url_helpers from engine’s routes. From various reasons it can’t be included automagically for engine’s controllers, but the plan is to make it work without any explicit includes. Why can’t we just use
blog_engine.posts_path? In that example it would work, but note that engine can be mounted with different
:as option. With mount looking like:
you would have to use
blog.posts_path instead of
blog_engine.posts_path. Basically engine should not need to know how it’s mounted. That said, we don’t have any information about options used to mount it, all that we know is what’s the request that was used to reach the engine.
But what about the generated path? Someone could say that it should also generate prefix, but that would not work as expected. Imagine that someone requested one of your users blog with path
"/dhh/blog/posts/1". When you click on link with url generated by
posts_path, you should stay in the same scope, so url should depend on you current path. This is achieved by using
env["SCRIPT_NAME"] value. In request to
"/dhh/blog/posts/1", the script name would be set to
"/dhh/blog", as this is the part of path that does not belong to engine. It should be clear now, that example above will generate
"/dhh/blog/posts" path for such request.
The next thing that is worth mentioning is
_routes method. When you call posts_path directly, it must use routes object. This object is available through
_routes method. This method is defined when you include
url_helpers, so it points to application’s routes by default. When
Blog::Engine.routes.url_helpers are included in PostsController,
_routes is changed to use engine’s routes, and because of that we can use posts_path safely.
We would like to generate root_path from our application’s router. Obviously it should generate
"/" path, without Engine’s prefix. The only exception is situation when app is hosted in a sub path (eg /myapp). This can be done with Phusion Passenger, using RailsBaseURI option.
/myapp part would be passed as
SCRIPT_NAME in such case. Nothing complicated, we already know how to use
SCRIPT_NAME, right? Not really (wouldn’t it be to simple? ;-). The problem is,
SCRIPT_NAME is kept as a string. Let’s see how the request to
"/myapp/user/blog/posts" looks like.
At first it hits the application.
SCRIPT_NAME is set to
/myapp by Passenger and the path is
/user/blog/posts. Now, application’s router recognizes that this request should get to engine, so
Blog::Engine is called. As engine needs only
/posts as path, the prefix (
/user/blog) will be attached to
env["SCRIPT_NAME"] resulting in
/myapp/user/blog. As this is one string and we need to get just application’s script name, solution is not obvious. How do we get the original script name? Right now our approach is to use whatever is set in
Rails.application.routes.default_url_options[:script_name], so it should be set to “/myapp” in that case.
In that case, url should be generated with prefix (which would be
/user/blog in my example). In ActionMailer we are not inside request, so
script_name is not available and with that in mind we need to generate the full path with
"/user/blog/" at the beginning.
Although it may look complicated to handle all those use cases, solution is relatively easy. The first thing to do is to assume that prefix should be always generated. This gets all cases with prefix out of our way. What’s not so obvious, it also makes generating application’s url in engine work. As application’s routes are not mounted, prefix is nil, so we can safely add it to “generate prefix” case. What’s left? Generating engine’s url inside engine. We need to use
env["SCRIPT_NAME"] here. How to check if we should attach script name? We need to check if routes used to generate url are the same as routes connected with current request.
Right now, to make it possible, router object is passed via
env["action_dispatch.routes"]. When application is called, it sets it to
Rails.application.routes and then when engine is called, it sets its own router there. That way, we always now in which controller are we. If
_routes method points to the same routes as the
env["action_dispatch.routes"] it means that we try to generate engine’s url inside engine and we should use the
The initial idea was to allow using named routes from 2 routers in one scope, just like that:
Although it would be handy if you will have to use paths from mounted engine a lot, it is the cause of many issues:
_routesmethod will point to second routes (
Blog::Engine.routesin last example), which will cause problems with using url helpers directly
There are some solutions for all of those problems, but as they’re all a bit hacky and complicated, we decided to leave that topic and not allow to use helpers from 2 routers in one scope. It means that you will have to explicitly say that you want to use router other then the default one (for example by using
blog_engine.posts_path helper). It also means that including url_helpers from engine will overwrite current routes, so in case above, you can’t directly use application routes any more. In such case you should use
I hope that this post is good introduction to current status of router usage in mountable apps. As usual: if you have any ideas, feature requests, critique or any other thoughts that could help bringing mountable apps to life, you’re more than welcome ;-)